🏗️Request data

We understand about router. What to do with requests now?

JSON

JSON marshaller/unmarshaller is hardcoded and cannot be changed. At the moment jsoniter is used, and it can be changed in the future.

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",
    })
}

Path queries

Path queries are key-value pairs, defined in the request path and is separated by question mark

// r.Get("/user", getUser)
func getUser(req *http.Request) *http.Response {
    name, err := req.Query.Get("id")
    if err != nil {
        return http.Error(req, err)
    }
    
    userModel := getUserByID(id)
    
    return http.JSON(userModel)
}

Query object by itself isn't keyvalue.Storage, but it can be obtained via query.Unwrap() method.

Most of the query methods are returning an error, because query parser is lazy: nothing will be parsed on no demand. So the first usage of it may return syntax error, however all others don't — only ErrNoSuchKey.

Headers

Headers, queries, dynamic path wildcards, cookies — almost every key-value storage uses the same underlying implementation — keyvalue.Storage. Therefore, all the methods used for headers can be used similarly for each of sources. Here's a short demonstration of what the storage can:

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()
} 

Body

You can use request.Body.String() or request.Body.Bytes() in order to get the whole body at once:

func myHandler(request *http.Request) *http.Response {
    // simple echo-server
    data, err := request.Body.String()
    if err != nil {
        return http.Error(request, err, status.ErrBadRequest)
    }
    
    return http.String(request, data)
}

request.Body.Bytes() does the same, but returns byte slice instead.

Alternatively, you can enable streaming-based processing. Body object implements io.Reader, so can be passed directly everywhere a reader is supported. You can rely on callbacks:

request.Body.Callback(func(data []byte) error {
    log.Println(string(data))
    return nil
})

Or you can retrieve part-by-part by your own:

for {
    data, err := request.Body.Retrieve()
    switch err {
    case nil:
    case io.EOF: // body completed
    default: // any other fatal error
    }
}

However, this way is considered low-level, and is primarily used by other methods.

If there used to be a body that wasn't read or was read partially after handling the request is done, it'll be discarded (you can think of it, as it would be drained into ioutil.Discard).

Hijacking

Hijacking a connection returns the actual tcp.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 tcp.Client is, it is being used internally, and any extra data is stored in it. In case some data was sent just after the request, it may be fully or partially lost because of that. However, you can get that pending data, unwrap and use the original net.Conn instance:

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.

Context

Each request object contains the ctx attribute with the type of context.Context inside. Its purpose is passing user values exclusively. It's cleaned after every request, so can be used in order to pass something via middlewares. For example, request ID, or similar. For example:

func reqID(next inbuilt.Handler, request *http.Request) *http.Response {
    request.Ctx = context.WithValue(request.Ctx, "request-id", uniqueID())
    
    return next(request)
}

func myHandler(request *http.Request) *http.Response {
    // simply return the unique ID in response
    return http.String(request.Ctx.Value("id").(string))
}

func main() {
    r := inbuilt.New().
        Get("/unique-id", myHandler, reqID)
    log.Fatal(indigo.New(":8080").Serve(r))
}

Last updated