The problem: Go's net/http is powerful and versatile, but using it correctly for client requests can be extremely verbose.
The solution: The requests.Builder type is a convenient way to build, send, and handle HTTP requests. Builder has a fluent API with methods returning a pointer to the same struct, which allows for declaratively describing a request by method chaining.
Requests also comes with tools for building custom http transports, include a request recorder and replayer for testing.
- Simplifies HTTP client usage compared to net/http
- Can't forget to close response body
- Checks status codes by default
- Supports context.Context
- JSON serialization and deserialization helpers
- Easily manipulate URLs and query parameters
- Request recording and replaying for tests
- Customizable transports and validators that are compatible with the standard library and third party libraries
- No third party dependencies
- Good test coverage
code with net/http | code with requests |
---|---|
req, err := http.NewRequestWithContext(ctx,
http.MethodGet, "http://example.com", nil)
if err != nil {
// ...
}
res, err := http.DefaultClient.Do(req)
if err != nil {
// ...
}
defer res.Body.Close()
b, err := io.ReadAll(res.Body)
if err != nil {
// ...
}
s := string(b) |
var s string
err := requests.
URL("http://example.com").
ToString(&s).
Fetch(ctx) |
11+ lines | 5 lines |
code with net/http | code with requests |
---|---|
body := bytes.NewReader(([]byte(`hello, world`))
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://postman-echo.com/post", body)
if err != nil {
// ...
}
req.Header.Set("Content-Type", "text/plain")
res, err := http.DefaultClient.Do(req)
if err != nil {
// ...
}
defer res.Body.Close()
_, err := io.ReadAll(res.Body)
if err != nil {
// ...
} |
err := requests.
URL("https://postman-echo.com/post").
BodyBytes([]byte(`hello, world`)).
ContentType("text/plain").
Fetch(ctx) |
12+ lines | 5 lines |
code with net/http | code with requests |
---|---|
var post placeholder
u, err := url.Parse("https://jsonplaceholder.typicode.com")
if err != nil {
// ...
}
u.Path = fmt.Sprintf("/posts/%d", 1)
req, err := http.NewRequestWithContext(ctx,
http.MethodGet, u.String(), nil)
if err != nil {
// ...
}
res, err := http.DefaultClient.Do(req)
if err != nil {
// ...
}
defer res.Body.Close()
b, err := io.ReadAll(res.Body)
if err != nil {
// ...
}
err := json.Unmarshal(b, &post)
if err != nil {
// ...
} |
var post placeholder
err := requests.
URL("https://jsonplaceholder.typicode.com").
Pathf("/posts/%d", 1).
ToJSON(&post).
Fetch(ctx) |
18+ lines | 7 lines |
var res placeholder
req := placeholder{
Title: "foo",
Body: "baz",
UserID: 1,
}
err := requests.
URL("/posts").
Host("jsonplaceholder.typicode.com").
BodyJSON(&req).
ToJSON(&res).
Fetch(ctx)
// net/http equivalent left as an exercise for the reader
// Set headers
var headers postman
err := requests.
URL("https://postman-echo.com/get").
UserAgent("bond/james-bond").
ContentType("secret").
Header("martini", "shaken").
Fetch(ctx)
u, err := requests.
URL("https://prod.example.com/get?a=1&b=2").
Hostf("%s.example.com", "dev1").
Param("b", "3").
ParamInt("c", 4).
URL()
if err != nil { /* ... */ }
fmt.Println(u.String()) // https://dev1.example.com/get?a=1&b=3&c=4
// record a request to the file system
var s1, s2 string
err := requests.URL("http://example.com").
Transport(requests.Record(nil, "somedir")).
ToString(&s1).
Fetch(ctx)
check(err)
// now replay the request in tests
err = requests.URL("http://example.com").
Transport(requests.Replay("somedir")).
ToString(&s2).
Fetch(ctx)
check(err)
assert(s1 == s2) // true
See wiki for more details.
Brad Fitzpatrick, long time maintainer of the net/http package, wrote an extensive list of problems with the standard library HTTP client. His four main points (ignoring issues that can't be resolved by a wrapper around the standard library) are:
- Too easy to not call Response.Body.Close.
- Too easy to not check return status codes
- Context support is oddly bolted on
- Proper usage is too many lines of boilerplate
Requests solves these issues by always closing the response body, checking status codes by default, always requiring a context.Context
, and simplifying the boilerplate with a descriptive UI based on fluent method chaining.
There are two major flaws in other libraries as I see it. One is that in other libraries support for context.Context
tends to be bolted on if it exists at all. Two, many hide the underlying http.Client
in such a way that it is difficult or impossible to replace or mock out. Beyond that, I believe that none have acheived the same core simplicity that the requests library has.
var data SomeDataType
err := requests.
URL("https://example.com/my-json").
ToJSON(&data).
Fetch(ctx)
body := MyRequestType{}
var resp MyResponseType
err := requests.
URL("https://example.com/my-json").
BodyJSON(&body).
ToJSON(&resp).
Fetch(ctx)
It depends on exactly what you need in terms of file atomicity and buffering, but this will work for most cases:
err := requests.
URL("http://example.com").
ToFile("myfile.txt").
Fetch(ctx)
For more advanced use case, use ToWriter
.
var s string
err := requests.
URL("http://example.com").
ToString(&s).
Fetch(ctx)
By default, if no other validators are added to a builder, requests will check that the response is in the 2XX range. If you add another validator, you can add builder.CheckStatus(200)
or builder.AddValidator(requests.DefaultValidator)
to the validation stack.
To disable all response validation, run builder.AddValidator(nil)
.
Please create a discussion before submitting a pull request for a new feature.