🦧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.
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