pod afButter

A library that helps ease HTTP requests through a stack of middleware

Mixins

Butter

Butter instances route HTTP requests through a stack of middleware.

ButterMiddleware

Implement to define middleware for Butter.

Classes

Body

Convenience methods for reading and writing content.

ButterDish

Holds an instance of Butter; use to create helper classes that access your middleware.

ButterRequest

The HTTP request.

ButterResponse

The HTTP response.

ErrOn4xxMiddleware

(Middleware) - Throws BadStatusErr when a HTTP response returns a 4xx status code.

ErrOn5xxMiddleware

(Middleware) - Throws BadStatusErr when a HTTP response returns a 5xx status code.

FollowRedirectsMiddleware

(Middleware) - Automatically resubmits requests on redirect responses.

GzipMiddleware

(Middleware) - Automatically un-gzips HTTP response content.

HttpRequestHeaders

A wrapper for HTTP request headers with accessors for commonly used headings.

HttpResponseHeaders

A wrapper for HTTP response headers with accessors for commonly used headings.

HttpTerminator

(Terminator) - A Butter Terminator for making real HTTP requests.

ProxyMiddleware

(Middleware) - Sets a proxy for HTTP sockets to connect via.

QualityValues

Parses a Str of HTTP qvalues as per HTTP 1.1 Spec / rfc2616-sec14.3.

StickyCookiesMiddleware

(Middleware) - Stores cookies found in response objects and sets them in subsequent requests.

StickyHeadersMiddleware

(Middleware) - Automatically sets headers in each request, so you don't have to!

Errs

BadStatusErr

Throw by ErrOnXxxMiddleware when a HTTP response returns a bad status code.

ButterErr

As thrown by Butter.

Overview

Butter is a library that helps ease HTTP requests through a stack of middleware.

Butter is a replacement for web::WebClient providing an extensible chain of middleware for making repeated HTTP requests and processing the responses. The adoption of the Middleware pattern allows you to seamlessly enhance and modify the behaviour of your HTTP requests.

Butter was inspired by Ruby's Faraday library.

Install

Install Butter with the Fantom Repository Manager ( fanr ):

C:\> fanr install -r http://repo.status302.com/fanr/ afButter

To use in a Fantom project, add a dependency to build.fan:

depends = ["sys 1.0", ..., "afButter 1.1"]

Documentation

Full API & fandocs are available on the Status302 repository.

Quick Start

1). Create a text file called Example.fan:

using afButter

class Example {
    Void main() {
        butter   := Butter.churnOut()
        response := butter.get(`http://www.fantomfactory.org/`)
        echo(response.body.str)
    }
}

2). Run Example.fan as a Fantom script from the command line:

C:\> fan Example.fan
<!DOCTYPE html>
<html>
    <head>
        <title>Home :: Fantom-Factory</title>
        ....
        ....

Usage

An instance of Butter wraps a stack of Middleware classes. When a HTTP request is made through Butter, each piece of middleware is called in turn. Middleware classes may either pass the request on to the next piece of middleware, or return a response. At each step, the middleware classes have the option of modifying the request and / or response objects.

The ordering of the middleware stack is important.

The last piece of middleware MUST return a response. These middleware classes are called Terminators. The default terminator is the HttpTerminator which makes an actual HTTP request to the interweb. (When testing this could be substituted with a mock terminator that returns mocked / canned responses.)

To create a Butter instance, call the static Butter.churnOut() method, optionally passing in a custom list of middleware:

middlewareStack := [
    StickyHeadersMiddleware(),
    GzipMiddleware(),
    FollowRedirectsMiddleware(),
    StickyCookiesMiddleware(),
    ErrOn4xxMiddleware(),
    ErrOn5xxMiddleware(),
    ProxyMiddleware(),
    HttpTerminator()
]

butter := Butter.churnOut(middlewareStack)

Or to use the default stack of middleware bundled with Butter, just churn and go:

html := Butter.churnOut.get(`http://www.fantomfactory.org/`).body.str

Butter Dishes

Because functionality is encapsulated in the middleware, you need to access these classes to configure them. Use the Butter.findMiddleware() method to do this:

butter := Butter.churnOut()
((FollowRedriectsMiddleware) butter.findMiddleware(FollowRedriectsMiddleware#)).enabled = false
((ErrOn5xxMiddleware) butter.findMiddleware(ErrOn5xxMiddleware#)).enabled = false

As you can see, this code is quite verbose. To combat this, there are two alternative means of getting hold of middleware:

Dynamic Stylie

If you make dynamic invocation method calls on the Butter class, you can retrieve instances of middleware. The dynamic methods have the same simple name as the middleware type. If the type name ends with Middleware, it may be omitted. Example:

butter := Butter.churnOut()
butter->followRedriects->enabled = true
butter->errOn5xx->enabled = true

Should instances of the same middleware class be in the stack more than once (or should it contain 2 middleware classes with the same name from different pods) then the just first one is returned.

Obviously, dynamic invocation should be used with caution.

Static Stylie

To call the middleware in a statically typed fashion, use a ButterDish class that holds your Butter instance and contains helper methods. There is a default ButterDish class with methods to access middleware in the default stack. Example:

butter := ButterDish(Butter.churnOut())
butter.followRedirects.enabled = true
butter.errOn5xx.enabled = true

When using other middleware, you are encouraged to create your own ButterDish that extends the default one.

Handling 404 and other Status Codes

If a 404, or a 4xx, status code is returned from a request then, by default, a BadStatusErr is thrown. The same goes for 500 and 5xx status codes. In general this is what you want, a fail fast approach to erroneous status codes. But during testing it is often desirable to disable the errors and check / verify the status codes yourself. To do so, just disable the required middleware:

using afButter

class TestStatusCodes {
    Void test404() {
        butter := ButterDish(Butter.churnOut())
        butter.errOn4xx.enabled = false
        res := butter.get(`http://www.google.com/404`)
        verifyEq(res.statusCode, 404)
    }

    Void test500() {
        butter := ButterDish(Butter.churnOut())
        butter.errOn5xx.enabled = false
        res := butter.get(`http://www.example.com/500`)  // insert failing URL here
        verifyEq(res.statusCode, 500)
    }
}

Calling RESTful Services

Butter has some convenience methods for calling RESTful services.

GET

For a simple GET request:

butter   := ButterDish(Butter.churnOut())
response := butter.get(`http://example.org/`)

POST

To send a POST request:

butter   := ButterDish(Butter.churnOut())
jsonObj  := ["wot" : "ever"]
response := butter.postJsonObj(`http://example.org/`, jsonObj)

PUT

To send a PUT request:

butter   := ButterDish(Butter.churnOut())
jsonObj  := ["wot" : "ever"]
response := butter.putJsonObj(`http://example.org/`, jsonObj)

DELETE

To send a DELETE request:

butter   := ButterDish(Butter.churnOut())
response := butter.delete(`http://example.org/`)

Misc

For complete control over the HTTP requests, create a ButterRequest and set the headers and the body yourself:

butter   := ButterDish(Butter.churnOut())
request  := ButterRequest(`http://example.org/`) {
    it.method = "POST"
    it.headers.contentType = MimeType("application/json")
    it.body.str = """ {"wot" : "ever"} """
}
response := butter.sendRequest(req)

Release Notes

v1.1.2

  • New: Body.form field for getting / setting URL encoded forms.
  • New: ButterResponse.makeFromBuf() ctor for Bounce.
  • Chg: Better gzip management in GzipMiddleware.

v1.1.0

  • New: ProxyMiddleware re-uses the proxy mechanism used by web::WebClient.
  • New: Added more REST methods to Butter.
  • Chg: ButterRequest and ButterResponse now share a common Body object. (Breaking change)
  • Chg: BadStatusErr displays more request / response details.

v1.0.6

  • Chg: Added ButterRequest.setBodyFromStr() and ButterRequest.setBodyFromJson().
  • Chg: Added ButterResponse.asJson() and ButterResponse.asJsonMap().
  • Bug: HttpTerminator sets Content-Length header for GET requests with a non-empty body.
  • Bug: GzipMiddleware updated to work with Fantom-1.0.67.

v1.0.4

v1.0.2

  • New: Added getCookie() and removeCookie() to StickyCookiesMiddleware.
  • Chg: HttpRequestHeaders.host is now a Str.

v1.0.0

  • New: Added GzipMiddleware.
  • Chg: Renamed ButterRequest.uri -> ButterRequest.url.
  • Chg: Request header Host is normalised.

v0.0.8

  • New: ErrOn5xxMiddleware detects and re-throws any Errs processed by BedSheet.
  • Chg: Rejigged the default middleware stack so Cookies can be captured in re-direct responses.

v0.0.6

  • Chg: Added support for HTTP 1.1 308 Redirects.

v0.0.4

  • New: Added ErrOn4xxMiddleware to cacth those annoying 404s!
  • Chg: Support for HTTP resposne headers that may appear multiple times, e.g. Set-Cookie
  • Chg: Renamed ButterRequest.data() -> stash().
  • Bug: Could not post case-insensitive forms - see Uri.encodeQuery throws UnsupportedOperationException

v0.0.2

  • New: Preview Release