🏗️Requests

Processing client requests is an essential part of the job. That sentence took more cognitive effort than it should have.

What can I do with a request

Except pretty much straightforward and standard attributes like Method, Path, Params, Headers the request entity also contains Remote , which holds the remote connection address, the environment Env , which is used as a context for relatively rare cases, and a context Ctx , which is given to you as it is and managed by you only. It won't even be cleared between requests.

The request entity acts more like a DTO, even though it's not completely true. The request body is also an independent entity. It provides the following leverages:

  • Callback(cb func([]byte) error)

    • Calls the passed callback every time a new chunk of data arrives

    • Can only be called once

  • Bytes() ([]byte, error)

    • Returns the full body all-at-once

    • Can be called multiple times

  • String() (string, error)

    • Similar to the Bytes() , but for strings

  • Read([]byte) (int, error)

    • Implements the io.Reader interface

  • JSON(mode any) error

    • Reads the full body and parses it into the model

      • Answering all obligations — even the encoding/json doesn't really support stream parsing.

    • Uses json-iterator/go under the hood. Soon it'll be able to be swapped.

  • Form() (form.Form, error)

    • form.Form is effectively just a []form.Data but with a few convenient lookup methods

    • Both x-www-form-urlencoded and multipart/form-data are supported

Other methods are irrelevant.

Usage examples

Here's a basic example of both receiving and uploading JSON:

func myHandler(request *http.Request) *http.Response {
    // parse JSON from the request. 
    // If body doesn't have a type of JSON or 
    // it's invalid, an error will be returned.
    got, err := request.JSON(myModel)
    if err != nil {
        return http.Error(request, err, status.ErrBadRequest)
    }
    
    return http.JSON(request, map[string]string{
        "ok": true,
        "hello": "world",
    })
}

Headers

Headers, parameters, dynamic path wildcards, cookies — almost every key-value storage uses the same underlying implementation — kv.Storage. Which in fact makes life a bit easier: the same set of methods is available everywhere. Here's a quick demo of what we can actually do with the storage:

func myHandler(req *http.Request) *http.Response {
    if !req.Headers.Has("some-header") {
        return http.Error(req, status.ErrForbidden)
    }
    
    header, found := req.Headers.Get("some-header")
    // or:
    header = req.Headers.Value("some-header")
    // get all the header values:
    values := req.Headers.Values("some-header")
    // get all the headers names contained:
    headers := req.Headers.Keys()
    // headers object will be re-used on a next request.
    // To use it after current request is processed,
    // you must clone it:
    headersCopy := req.Headers.Clone()
} 

Hijacking

Hijacking a connection returns the net.Client, which wraps the connection, and an error. An error is returned, because during the hijacking the request's body is drained automatically. The reason hjiacking returns the net.Client is, it implements a pushback - extra data read from the connection is stored back in order to be read the next time.

func hijacker(request *http.Request) *http.Response {
    client, err := request.Hijack()
    if err != nil {
        return http.Error(request, err)
    }
    
    pending := client.Pending()
    conn := client.Conn()
    ...
    
    return nil
}

Please, note that there is no need to close the connection. It'll be closed automatically as the handler exits.

Last updated