Rendered at 08:16:26 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
giancarlostoro 12 hours ago [-]
I do wonder when they'll add http/3 into the stdlib. Go having simple but powerful / capable out of the box support for http is one of the key things it had that made it get farther than it could have had it not included such a strong OOTB standard library. I feel like it would have still exploded but not quite as quickly, there's just something about building full web apps with no third party packages using only what's out of the box with Go itself, the only other "language" that I ever felt this way with was probably PHP, although Racket might be another, since I don't think I need to go out of my way to setup anything to do web dev with Racket.
zer00eyz 1 hours ago [-]
> the only other "language" that I ever felt this way with was probably PHP
I made a lot of money on PHP and more on Golang.
PHP had warts (variable order) but much like go it came with just about everything you wanted.
It isnt just the functionality - it's the fact that both of them had amazing documentation with the functionality. PHP manual was an amazing reference and the "comments" section always had a bit of solid wisdom for ya (and you needed it cause of the warts). Go's manual (and the blog) are such a fine resource that I rarely need to reach further than what it contains.
It's much easier to use what's included when the documentation is accessible and clear.
jerf 14 hours ago [-]
Am I missing something or is none of this information documented in net/http itself? This is a surprising implementation of "Close" and should be documented somewhere.
I can attest to the fact this is a Frequently Asked Question among people who are trying to Go as an HTTP client, so even though this feels somewhat icky, I can understand where it is coming from. However, I'm also missing, if I want to slam this connection closed, which is a reasonable thing to want, how do I do that without uselessly draining a socket I don't intend to reuse? Fake the "Connection: close" header into the Response object? Hope I can dig out an underlying socket of some sort under the concrete type of the Body.Response that I can bypass this code with? Seems like there should be a way to do that too.
jchw 14 hours ago [-]
I think it is reasonable for it to not be documented directly on Close. Close is arguably too high level for this: for example, on HTTP/2 this doesn't do what you would intend at all, since the request lifecycle is completely untethered from any underlying socket. If you really wanted to slam the connection closed ASAP it does actually seem more logical to grab the underlying connection and directly force it closed, even though this feels uglier since it requires knowing the underlying layers. To me this "Close" is logically from the perspective of the HTTP request itself and if I'm not getting myself confused and misunderstanding it doesn't really close the connection anyways (e.g. if there is no body and the connection is keep-alive'd, this wouldn't close the underlying connection, right?)
> Note, however, that Transport will automatically try to read a Response Body to EOF asynchronously up to a conservative limit when a Body is closed.
Similar wording appears in a few other places; search for “asynchronously”.
jerf 14 hours ago [-]
Oh, good. Not exactly where I expected it but it's there at least.
hnav 12 hours ago [-]
HTTP libraries in general are tough because HTTP in practice requires understanding of at least DNS and TLS and in a lot of cases L4 networking as well. If you try to target application developers you end up with leaky abstractions (unread H/1 body draining behavior when you don't even know what HTTP version you're using) or a library low level enough that application developers don't know how to use it. See rust's hyper and reqwest as an example of an attempt at that split.
stingraycharles 9 hours ago [-]
As someone who once attempted implementing a http library, I can assure you that the protocol itself is at least equally tough as being aware of all the TLS / L4 edge cases.
I’d argue that implementing both TCP and IP from scratch is easier than implementing an HTTP library.
0x696C6961 12 hours ago [-]
A CloseNow() error method would make sense.
gandreani 13 hours ago [-]
I find it kind of amusing to consider that the go team didn't commit to implement this automatic connection re-use until they probably encountered in the wild the horrible but correct third iteration of drainAndCloseBody.
1. Suggest drainAndCloseBody // Team: this is reasonable
2. Implement drainAndCloseBodyWithTimer // Team: no problems here
3. then drainAsyncAndCloseBodyWithTimer // Team: a bit wordy but no problem here
4. Someone at google uses 3. with an http/2 connection // Team: this is where we draw the line?
I sympathize with the struggle of following golang's mission to be simple when you consider this common but tricky edge case. Iteration 3 makes sense but by gosh is it ugly ha.
rkuska 14 hours ago [-]
Now if you use json’s Decoder you can read a body and still might end up without reused connection - if a response contains a newline.
Decoder only reads X bytes (512? I don’t remember) and if the structure fits into these bytes exactly (and the multiplications of it) the newline leaves hanging there.
I always check for Decoder.More (this drains whitespace) after decoding.
hxtk 11 hours ago [-]
While it’s convenient, the Decoder isn’t recommended for those use cases precisely because of the edge cases around readers that don’t contain at most one JSON document with no suffixes.
The standard library maintainers recognized this as a problem that there was no “better,” more convenient way to do it and in encoding/json/v2 they have UnmarshalRead for that use case: https://pkg.go.dev/encoding/json/v2#UnmarshalRead
hnav 12 hours ago [-]
It drains up to 256K unread bytes period, it doesn't just optimize the case of the body not being read at all.
Oof, that is an edge case I wasn't aware of at all. Hard to know where it's documented since it's an integration of both those packages
cyphar 9 hours ago [-]
This can happen without HTTP, we hit this issue in runc a decade ago[1] -- some container invocations would mysteriously error out with ECONNRESET and if you added a dummy environment variable they would work again.
Turns out that json.Encoder always appends a newline but (as GP said) json.Decoder will not attempt to read it if the JSON is complete at the power-of-two byte boundary (IIRC the buffer doubles for the first few iterations). The extra byte in the pipe when closed then causes reads and writes to go boom. We switched to using json.Marshal because it doesn't output a newline (sadly the encoded form isn't streamed as a result).
I still get twitchy around encoding/json as a result of that experience.
I can finally remove a similar helper that I copied from project to project and from company to company since r60
jamesponddotco 14 hours ago [-]
I built that package exactly to stop copying code from project to project.
Not because of the copy itself, since a little copy is better than a little dependency, as they say, but because it was a pain to update everything when I inevitably found a bug in what I copied.
finalhacker 18 minutes ago [-]
old bug = new feat...
nasretdinov 15 hours ago [-]
It sucks that this changes the behaviour under the hood that you previously coukd rely on the fact that closing the response body lets you skip downloading the HTTP body entirely, but I do like the tradeoff here, since it also limits the download time to just 50ms you'd not get overwhelmed if your network can't keep up with all the cancelled HTTP bodies, you'll just wait a little bit of traffic instead.
At the very end it mentions it won't drain bodies for Connection: close
Does this work with reverse proxies?
I've had issues with nginx still getting IO errors when my server doesn't consume bodies and bubbling them up as 5xx codes to the client.
I believe I've seen this with Firefox as well, where XHR status codes would be 0.
Early closing, without consuming body, seems to work as expected with Chrome and curl though.
nulltrace 10 hours ago [-]
[dead]
donatj 14 hours ago [-]
> Using HTTP/1 (GODEBUG=http2client=0), calling this function 100 times against index.golang.org takes ~710 milliseconds on Go 1.26 on my machine. On Go 1.27, the same program takes ~150 milliseconds.
Does this mean I need to disable http2 for this to work or is that just a further proof that http2 isn't actually being used?
giovannibajo1 14 hours ago [-]
As explained in the post, you don’t need any of this in HTTP/2, since you get the benefits of connection sharing without having to read the drain connection body by protocol design
MrDOS 14 hours ago [-]
Right. Although the article demonstrates the changes by making requests of a public web server, I would imagine this change has been primarily motivated by a desire to reduce overhead and latency on internal communication between microservices, which very often uses unencrypted HTTP/1 traffic.
80fj0348i 14 hours ago [-]
HTTP/2 works differently, you can multiplex and prioritize different data over one TCP connection with HTTP/2. But 60-70% of the web is still HTTP/1 IIRC.
If you don't need SSL you can't use HTTP/2 or HTTP/3. It doesn't make sense for externally facing services but sometimes is ok or even unavoidable for local networks
cyberax 12 hours ago [-]
You can use HTTP/2 without encryption - it's called `h2c`. It's not technically standards-compliant and browsers don't support it, but it's used a lot for proxy-to-backend communication where traffic goes over localhost and/or over an internal private network.
creatonez 4 hours ago [-]
HTTP/2 without encryption is still standards compliant if you initiate the connection with prior knowledge that the protocol is supported. It's upgrading from HTTP/1.1 that isn't supposed to be supported.
But of course, the only scenarios where you have prior knowledge are things like reverse proxies or custom made clients.
13 hours ago [-]
hnav 12 hours ago [-]
It's mostly the property of servers and clients that you interoperate with that don't speak H/2. There are other situations too like if you run a CDN and you're doing sendfile with kernel TLS, you can do stuff like the below
which wouldn't work with H2/H3. WebSockets are still in practice H/1 only (spec exists but is disabled in browsers). Another aspect is that if you're simply using HTTP as a preamble to 1 large (bidi) pipe, the extra framing and flow control of H/2 is just a performance handicap.
nemothekid 4 hours ago [-]
I haven't checked in a while, but I believe nginx does not support http/2 with proxy_pass.
siren2026 12 hours ago [-]
believe it or not but Cloudfront only supports HTTP/1 to origin. Some other CDNs as well.
afavour 13 hours ago [-]
You’re requesting from a third party backend you don’t control?
trgn 13 hours ago [-]
imo, it's obvious for people writing ajax-y web apps, but backend devs need convincing.
I made a lot of money on PHP and more on Golang.
PHP had warts (variable order) but much like go it came with just about everything you wanted.
It isnt just the functionality - it's the fact that both of them had amazing documentation with the functionality. PHP manual was an amazing reference and the "comments" section always had a bit of solid wisdom for ya (and you needed it cause of the warts). Go's manual (and the blog) are such a fine resource that I rarely need to reach further than what it contains.
It's much easier to use what's included when the documentation is accessible and clear.
I can attest to the fact this is a Frequently Asked Question among people who are trying to Go as an HTTP client, so even though this feels somewhat icky, I can understand where it is coming from. However, I'm also missing, if I want to slam this connection closed, which is a reasonable thing to want, how do I do that without uselessly draining a socket I don't intend to reuse? Fake the "Connection: close" header into the Response object? Hope I can dig out an underlying socket of some sort under the concrete type of the Body.Response that I can bypass this code with? Seems like there should be a way to do that too.
> Note, however, that Transport will automatically try to read a Response Body to EOF asynchronously up to a conservative limit when a Body is closed.
Similar wording appears in a few other places; search for “asynchronously”.
I’d argue that implementing both TCP and IP from scratch is easier than implementing an HTTP library.
1. Suggest drainAndCloseBody // Team: this is reasonable
2. Implement drainAndCloseBodyWithTimer // Team: no problems here
3. then drainAsyncAndCloseBodyWithTimer // Team: a bit wordy but no problem here
4. Someone at google uses 3. with an http/2 connection // Team: this is where we draw the line?
I sympathize with the struggle of following golang's mission to be simple when you consider this common but tricky edge case. Iteration 3 makes sense but by gosh is it ugly ha.
Decoder only reads X bytes (512? I don’t remember) and if the structure fits into these bytes exactly (and the multiplications of it) the newline leaves hanging there.
I always check for Decoder.More (this drains whitespace) after decoding.
The standard library maintainers recognized this as a problem that there was no “better,” more convenient way to do it and in encoding/json/v2 they have UnmarshalRead for that use case: https://pkg.go.dev/encoding/json/v2#UnmarshalRead
https://cs.opensource.google/go/go/+/refs/tags/go1.27.2:src/...
What it won't do looking at the code is handle streamed responses over transfer-encoding:chunked which means that if the other end is doing
this optimization won't do anything.Turns out that json.Encoder always appends a newline but (as GP said) json.Decoder will not attempt to read it if the JSON is complete at the power-of-two byte boundary (IIRC the buffer doubles for the first few iterations). The extra byte in the pipe when closed then causes reads and writes to go boom. We switched to using json.Marshal because it doesn't output a newline (sadly the encoded form isn't streamed as a result).
I still get twitchy around encoding/json as a result of that experience.
[1]: https://github.com/docker/docker/issues/14203#issuecomment-1...
[1]: https://github.com/cipherdothost/x-go/blob/caa562ded82b8c417...
Not because of the copy itself, since a little copy is better than a little dependency, as they say, but because it was a pain to update everything when I inevitably found a bug in what I copied.
Does this mean I need to disable http2 for this to work or is that just a further proof that http2 isn't actually being used?
But of course, the only scenarios where you have prior knowledge are things like reverse proxies or custom made clients.