🏗️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 methodsBoth
x-www-form-urlencoded
andmultipart/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