🦧Basic usage

In case you decided to make your helloworld-app fancier

A handler is the heart of everything. However, unlike net/http or fasthttp, a handler takes a request and returns a response. The response is simply a builder object which is to be serialized into the actual HTTP response later, thereby bringing some implications, e.g. what to do if a response body reader returns io.EOF earlier than was supposed to?

It might be a bit easier to catch such errors controlling the response write stage manually, however at some cost of boilerplate. Accepting a little less control over the process gives you the essence of indigo - ease of use and laconic code.

Request

The request entity, http.Request, holds all the request-related information (how surprisingly.) Pay attention to the fact that a request method as well as a protocol rather than directly a string, even though both are fmt.Stringer-compatible. All supported request methods and protocol versions are stored in the indigo/http/method and indigo/http/proto packages respectively.

Headers are stored in the *kv.Storage entity, which is an associative key-value structure supporting a rich set of methods. All the headers are stored strictly in their order of appearance. The lookup is universally case-insensitive, but keys aren't normalized if accessing them directly (via Expose() method, for example.) Params (path query) and Vars (dynamic routing wildcard values) are all *kv.Storage under the hood, too.

Some of common and compelling headers are stored directly in the request object, even though they still appear in Headers.

Request body is a separate entity, allowing to process the actual body in multiple ways:

Callback

You can simply pass a closure. It'll be called every time a new chunk of body arrives. If an error occurs during processing the body or the closure returns one, it'll be returned back from the request.Body.Callback() .

func handler(request *http.Request) *http.Response {
    err := request.Body.Callback(func(chunk []byte) error {
        fmt.Println(string(chunk))
        return nil
    })
    ...
}

Reader

Body entity implements the io.Reader interface.

func handler(request *http.Request) *http.Response {
    decoder := json.NewDecoder(request.Body)
    ...
}

Fetcher

A low-level primitive, used mostly by other methods. Provides an io.Reader-like interface, but returns a piece of internal read buffer instead.

func handler(request *http.Request) *http.Response {
    for {
        chunk, err := request.Body.Fetch()
        if err != nil { ... }
        
        fmt.Println(string(chunk))
    }
    ...
}

String/Bytes

Reads the whole body into an internal buffer and returns it all-at-once.

func handler(request *http.Request) *http.Response {
    body, err := request.Body.String()
    if err != nil { ... }
    
    fmt.Println(body)
}

Form

Both application/x-www-form-urlencoded and multipart/form-data requests can be directly parsed into a form:

func handler(request *http.Request) *http.Response {
    form, err := request.Body.Form()
    sweets := form.File("sweets.txt")
    ...
}

JSON

func handler(request *http.Request) *http.Response {
    err := request.Body.JSON(&myModel)
}

Form data

request.Body.Form() returns a form.Form entity. It is effectively just a slice of form.Data structs, but with a few selectors to ease the usage.

Selectors

form, err := request.Body.Form()

username := form.Name("username")
avatar := form.File("avatar.png")

Both Name() and File() selectors return the first matching form.Data. There are also Names() and Files() (both return iter.Seq[form.Data]) in order to iterate over all matching data entries.

Data

Form data struct is defined in the following way:

type Data struct {
    Name     string
    Filename string
    Type     string
    Charset  string
    Value    string
}

Context

The request object also has a Ctx context.Context field, intended for communication between middlewares and a handler. It isn't used by indigo anyhow, except it's cleared before every request, making it impossible to persistently store data across requests made from a single specific connection.

Response

As said, response entity is in its essence a builder. It supports chaining. Moreover, chaining is encouraged:

func handler(request *http.Request) *http.Response {
    return http.
        Code(status.OK).
        Header("X-Request-ID", uuid.New()).
        String("<h1>success</h1>")
}

The indigo/http package contains a plenty of such methods. However, they are just shorthands for request.Respond().Code... .

The String() method is also a shorthand for SizedStream(strings.NewReader(...), len(...)) .

Errors

In order to make life a bit easier, there's a method to return errors:

func handler(request *http.Request) *http.Response {
    err := ...
    
    return http.Error(request, err)
}

It's convenient, because errors returned by indigo components are always status.HTTPError. The Error() automatically recognizes them and sets a proper response code. If the error isn't status.HTTPError , then simply status.InternalServerError is returned.

If the passed error is nil, the method does nothing. So the following code is perfectly fine:

func handler(request *http.Request) *http.Response {
    body, err := request.Body.String()
    
    return http.
        String(body).
        Error(err)
}

If an error occurred, it'll be correctly displayed. The request body is echoed back otherwise.

By the way, there's a cleaner and more efficient way to echo the request body back using streams:

func handler(request *http.Request) *http.Response {
    return http.Stream(request, request.Body)
}

Streams

func handler(request *http.Request) *http.Response {
    data := strings.NewReader("Hello, world!")
    
    return http.Stream(request, data)
}

Response body is called a stream, because it's received and processed as an io.Reader .

Internally, streams are divided into two categories: sized and unsized. The only difference is the method to write it: unsized are forced to use the chunked transfer encoding. However, sized streams can still be converted to an unsized, if, for example, a compression is applied.

nil stream returns status.ErrInternalServerError, unless it has the length of 0.

If the stream is also an io.Closer, it'll be closed.

http.Stream takes an optional size hint. If the hint isn't set (or set to the default value of -1), the stream is examined to have the Len() int method.

Compression

Codecs are registered via the app.Codec() method. Once registered, they are automatically enlisted in the Accept-Encoding header. Request bodies are decompressed automatically.

Unrecognized compression tokens in requests cause the connection to be actively closed.

On the other hand, a response must be compressed explicitly. You can either force a specific codec to be used (including "" and "identity" be both valid but no-op options):

func handler(request *http.Request) *http.Response {
    return http.
        String(request, "Hello, world!").
        Compression("gzip")
}

Or let the decision be made automatically:

func handler(request *http.Request) *http.Response {
    return http.
        String(request, "Hello, world!").
        Compress()
}

When the compression is decided automatically, its decision is based on client preferences and the body size. The compressor is automatically picked from the client's Accept-Encoding , which is checked against the list of all available codecs respecting q-values (and ordering when there are no q-values).

Bodies smaller than 4096 bytes (can be adjusted in the config.NET.SmallBody parameter) aren't compressed and are transferred plain. The reason here is performance: first, compressing isn't free. Second, transferring plain body is faster using zero-copy capabilities when available (e.g. sendfile(2) for Linux).

All out-of-box codecs are stored in the indigo/http/codec package. The following codecs are available:

  • gzip

  • deflate

  • zstd

The list might be extended in the future. In order to not worry about registering each one separately, you can simply do app.Codec(codec.Suit()...) .

Last updated