Johannes, On 05 Sep 2011, at 12:34, Johannes Rasche wrote:
> Hi Sven, I'm eagerly looking forward. > > Is this an announcement about what you're actually doing, or is it open for > experiments? Both I guess. > Is there already a repository I can load? Yes, just load the latest version of Zn (see http://zn.stfx.eu) > Johannes Thanks for your interest! Sven > Am 04.09.2011 um 20:22 schrieb Sven Van Caekenberghe: > >> Towards better HTTP client usage >> >> At the last Pharo Sprint in Lille (July 8th), Stéphane and I were at one >> point trying to remove some old HTTPSocket usage (which indirectly uses Zn) >> and replace it with direct and clean Zn usage. We hadn't much time left and >> didn't get very far. But I realized afterwards that although technically >> everything was there to write good HTTP client code, it was way too >> difficult (it required to much code on the user's behalf). >> >> Hence I decided to try write a new client that would support this usage much >> better and encourage better HTTP client usage in general. Another goal of >> this new client is to ultimately replace all other clients currently in Zn >> (these different client are creating some confusion among users). >> >> ZnNeoClient (a temporary name, to be renamed to ZnClient in the future) is a >> single object with a lot of (convenience) API to build, execute and process >> HTTP client requests. It is somewhat similar to Gofer like classes. It is >> also somewhat comparable to ZnHttpClient but it contains even more >> functionality. >> >> The new tests in ZnNeoClientTests show some of the ways the client can be >> used. In the current Zn version all ZnClient class functionality as well as >> all ZnHTTPSocketFacade (and thus HTTPSocket and thus the rest of the image) >> has already been reimplemented using ZnNeoClient. These are good examples to >> look at. >> >> At one point I plan to write some proper documentation, when the design >> settles down a bit, but here are some examples: >> >> The simplest possible usage: >> >> ZnNeoClient new get: 'http://zn.stxfx.eu/zn/small.html'. >> >> This is shorthand for: >> >> ZnNeoClient new >> url: 'http://zn.stxfx.eu/zn/small.html'; >> get. >> >> Which is actually shorthand for: >> >> ZnNeoClient new >> url: 'http://zn.stxfx.eu/zn/small.html'; >> get; >> contents. >> >> If you know upfront that you will do only one request, you can help conserve >> resources by doing: >> >> ZnNeoClient new >> beOneShot; >> get: 'http://zn.stxfx.eu/zn/small.html'. >> >> Connections are kept open whenever possible unless #beOneShot is chosen. >> Multiple requests (possibly to the same host) can be issued using the same >> client instance. At the end, #close should be send to the client, but >> garbage collection cleans up as well. >> >> Specifying URLs as strings can sometimes be tricky when special characters >> such as spaces are involved, for this ZnNeoClient has a whole URL and >> request construction API. Here is an example: >> >> ZnNeoClient new >> http; >> host: 'www.google.com'; >> addPath: 'search'; >> queryAt: 'q' put: 'Pharo Smalltalk'; >> get. >> >> There is also API for specifying headers, forms, multipart uploads and so >> on. See the unit tests, the ZnClient class side as well as the >> ZnHTTPSocketFacade for examples. >> >> The result of an HTTP request is an HTTP response, modelled by ZnResponse. >> Hence, you can ask a client for the last #response, #entity, #contents or >> test for #isSuccess. >> >> Which brings us to the problem that it is not easy to properly set up and >> handle the various things that can go wrong. It is here that ZnNeoClient >> aims to make a big contribution. >> >> Assume the following example: at some URL there is a list of numbers as >> lines in a text file that we want to download and use. Let's start simple: >> >> ^ ZnNeoClient new >> get: 'http://www.example.com/numbers.txt'. >> >> When all is well, this will return the text file as string. Let's add >> parsing: >> >> ^ ZnNeoClient new >> contentReader: [ :entity | >> (entity contents lines do: [ :each | >> Integer readFrom: each ifFail: [ nil ] ]) >> select: [ :each | each notNil ] ]; >> get: 'http://www.example.com/numbers.txt'. >> >> This will make sure we get a (possibly empty) list of numbers. The problem >> is, when number.txt is not found by the server, we don't deal properly with >> that situation. We can fix that: >> >> ^ ZnNeoClient new >> enforceHttpSuccess: true; >> contentReader: [ :entity | >> (entity contents lines do: [ :each | >> Integer readFrom: each ifFail: [ nil ] ]) >> select: [ :each | each notNil ] ]; >> get: 'http://www.example.com/numbers.txt'. >> >> Now, a non-success code will throw an error. We should also make sure that >> we do get a text/plain document back, and we want a uniform error handler >> reaction: >> >> ^ ZnNeoClient new >> enforceHttpSuccess: true; >> enforceAcceptContentType: true; >> accept: ZnMimeType textPlain; >> contentReader: [ :entity | >> (entity contents lines do: [ :each | >> Integer readFrom: each ifFail: [ nil ] ]) >> select: [ :each | each notNil ] ]; >> ifFail: [ :exception | >> self log: exception printString, ' while fetching numbers'. >> ^ #() ]; >> get: 'http://www.example.com/numbers.txt'. >> >> The failBlock will be execute on any exception, including ZnHttpUnsuccessful >> and ZnUnexpectedContentType. >> >> What about unreliable networking in general: we need a timeout, but better >> still, we could retry the request once or twice to cover for (possibly >> transient) networking/server problems. >> >> ^ ZnNeoClient new >> timeout: 15; >> numberOfRetries: 1; >> retryDelay: 2; >> enforceHttpSuccess: true; >> enforceAcceptContentType: true; >> accept: ZnMimeType textPlain; >> contentReader: [ :entity | >> (entity contents lines do: [ :each | >> Integer readFrom: each ifFail: [ nil ] ]) >> select: [ :each | each notNil ] ]; >> ifFail: [ :exception | >> self log: exception printString, ' while fetching numbers'. >> ^ #() ]; >> get: 'http://www.example.com/numbers.txt'. >> >> If anything goes wrong, there will be one retry after a delay of 2 seconds. >> >> Although there are sensible defaults for all options, it will probably make >> sense to group the options, like this: >> >> ^ ZnNeoClient new >> systemPolicy; >> accept: ZnMimeType textPlain; >> contentReader: [ :entity | >> (entity contents lines do: [ :each | >> Integer readFrom: each ifFail: [ nil ] ]) >> select: [ :each | each notNil ] ]; >> ifFail: [ :exception | >> self log: exception printString, ' while fetching numbers'. >> ^ #() ]; >> get: 'http://www.example.com/numbers.txt'. >> >> Finding the proper defaults and policies will take some trial and error. >> Another strategy is factory method like ZnClient class>>#client or >> ZnHTTPSocketFacade class>>#client. >> >> Here is another real-world example, invoking a REST service that returns >> JSON (you need the JSJsonParser class that comes with Seaside): >> >> ^ ZnNeoClient new >> systemPolicy; >> beOneShot; >> url: 'http://easy.t3-platform.net/rest/geo-ip'; >> queryAt: 'address' put: '81.83.7.35'; >> accept: ZnMimeType applicationJson; >> contentReader: [ :entity | >> (JSJsonParser parse: entity contents) at: #country ]; >> ifFail: [ nil ]; >> get. >> >> Or as a utility method: >> >> XYZUtils class>>#countryForIpAddress: ipAddressString ifFail: failBlock >> ^ ZnNeoClient new >> systemPolicy; >> beOneShot; >> url: 'http://easy.t3-platform.net/rest/geo-ip'; >> queryAt: 'address' put: ipAddressString; >> accept: ZnMimeType applicationJson; >> contentReader: [ :entity | >> (JSJsonParser parse: entity contents) at: #country ]; >> ifFail: failBlock; >> get >> >> That's it for now. There is still some implementation and testing work to be >> done. As always, all feedback is welcome. >> >> Sven
