yamine 0.23.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/lib/yamine/proxy.rb +98 -9
- data/lib/yamine/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c868efafc13a1f8bf3aadad4cf4d0cacffe110e51e091e30b79eb1e81d2f4a24
|
|
4
|
+
data.tar.gz: f888229b7d618e2936108a27342f9227c8f986e46611ba22ea880052ca190a49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ad556cdad8d143e9beb87ba0cdcfc2fa5a686ad04d10ad763e048f4ace90d084527ecc23064998ac37d9a65705e831b3216675ab5306a5da8b869680f302371
|
|
7
|
+
data.tar.gz: b1b11d113410b72139b9f8ea19592342b29b40edab39869861e2488f4c4c8f193de00ad564f2ba771dba5e2a33072abd41c5e0f10c649dd0ff55bceaaeab98ff
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.23.1] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A chunked response now ends at its terminator instead of at end of
|
|
10
|
+
file.** The relay had one way to end a body with no Content-Length —
|
|
11
|
+
copy until the backend hung up — and it read every such body as
|
|
12
|
+
close-delimited. But a chunked body ends at its terminating chunk, and
|
|
13
|
+
the backend holding the connection open past it is keep-alive doing
|
|
14
|
+
exactly what it promised. So the relay blocked for the whole idle
|
|
15
|
+
bound, sixty seconds by default, with the client's connection pinned
|
|
16
|
+
the entire time. Every streamed page, every ActionController::Live
|
|
17
|
+
response and every event stream is framed this way, and the request
|
|
18
|
+
that followed on that connection — typically the page's main
|
|
19
|
+
JavaScript chunk — got nothing back until the connection was reaped.
|
|
20
|
+
A load test could not see it, because every request in one opened its
|
|
21
|
+
own connection. Chunked framing is now read rather than assumed, bytes
|
|
22
|
+
still cross to the client exactly as the backend wrote them, and the
|
|
23
|
+
connection is free for the next request the moment the terminator
|
|
24
|
+
arrives.
|
|
25
|
+
- `minitest/mock` is required by the test helper, so `Object#stub` is
|
|
26
|
+
available to every test rather than only the ones that load it.
|
|
27
|
+
|
|
5
28
|
## [0.23.0] - 2026-10-02
|
|
6
29
|
|
|
7
30
|
### Fixed
|
data/lib/yamine/proxy.rb
CHANGED
|
@@ -64,6 +64,7 @@ module Yamine
|
|
|
64
64
|
# which is also the only one a retry can be safe for.
|
|
65
65
|
HeadTimeout = Class.new(IdleTimeout)
|
|
66
66
|
CHUNK_BYTES = 16_384
|
|
67
|
+
CRLF = "\r\n"
|
|
67
68
|
# Route cache: routes.json is the source of truth, but re-reading
|
|
68
69
|
# and re-parsing it on every request is wasteful under HMR polling.
|
|
69
70
|
# Keyed on file mtime, so a freshly registered route is visible on
|
|
@@ -449,16 +450,26 @@ module Yamine
|
|
|
449
450
|
def relay_after_head(sock, backend, rhead, rbuf, rheaders, headers, target)
|
|
450
451
|
begin
|
|
451
452
|
write_all(sock, rhead)
|
|
452
|
-
write_all(sock, rbuf) unless rbuf.empty?
|
|
453
453
|
|
|
454
454
|
rlen = content_length(rheaders)
|
|
455
|
-
if
|
|
456
|
-
#
|
|
457
|
-
|
|
458
|
-
|
|
455
|
+
if chunked?(rheaders)
|
|
456
|
+
# The chunk relay reads the framing to find where the body ends,
|
|
457
|
+
# so it takes the read-ahead itself — handing it bytes already on
|
|
458
|
+
# the wire would send the body twice.
|
|
459
|
+
relay_chunked(backend, sock, rbuf)
|
|
460
|
+
else
|
|
461
|
+
write_all(sock, rbuf) unless rbuf.empty?
|
|
462
|
+
|
|
463
|
+
if rlen.nil?
|
|
464
|
+
# No Content-Length and no chunk framing: the body really does
|
|
465
|
+
# end at end of file, because the backend is closing to say so.
|
|
466
|
+
copy_stream(backend, sock)
|
|
467
|
+
return [:close, nil]
|
|
468
|
+
else
|
|
469
|
+
remaining = rlen - rbuf.bytesize
|
|
470
|
+
copy_stream(backend, sock, remaining) if remaining > 0
|
|
471
|
+
end
|
|
459
472
|
end
|
|
460
|
-
remaining = rlen - rbuf.bytesize
|
|
461
|
-
copy_stream(backend, sock, remaining) if remaining > 0
|
|
462
473
|
rescue EOFError
|
|
463
474
|
@on_error.call("backend for #{headers["host"]}#{target} closed mid-body, " \
|
|
464
475
|
"before its Content-Length of #{rheaders["content-length"]} was met")
|
|
@@ -486,8 +497,13 @@ module Yamine
|
|
|
486
497
|
return if rhead.nil?
|
|
487
498
|
|
|
488
499
|
write_all(sock, rhead)
|
|
489
|
-
|
|
490
|
-
|
|
500
|
+
_rm, _rt, rheaders = parse_head(rhead)
|
|
501
|
+
if chunked?(rheaders)
|
|
502
|
+
relay_chunked(backend, sock, rbuf)
|
|
503
|
+
else
|
|
504
|
+
write_all(sock, rbuf) unless rbuf.empty?
|
|
505
|
+
copy_stream(backend, sock)
|
|
506
|
+
end
|
|
491
507
|
rescue IOError, SystemCallError
|
|
492
508
|
nil
|
|
493
509
|
end
|
|
@@ -652,6 +668,79 @@ module Yamine
|
|
|
652
668
|
end
|
|
653
669
|
end
|
|
654
670
|
|
|
671
|
+
# Chunked framing is the one body the proxy has to read rather than
|
|
672
|
+
# copy, because it is the only thing that says where the body ends. It
|
|
673
|
+
# ends at the terminating chunk — not at end of file, and the backend
|
|
674
|
+
# holding the connection open past it is keep-alive doing exactly what
|
|
675
|
+
# it promised. Every streamed page, every ActionController::Live
|
|
676
|
+
# response and every event stream is framed this way.
|
|
677
|
+
#
|
|
678
|
+
# Bytes cross to the client exactly as the backend wrote them; only the
|
|
679
|
+
# framing is read, never rewritten. `buf` is the read-ahead already
|
|
680
|
+
# past the response head.
|
|
681
|
+
def relay_chunked(src, dst, buf)
|
|
682
|
+
loop do
|
|
683
|
+
line = take_line(src, buf)
|
|
684
|
+
return if line.nil?
|
|
685
|
+
|
|
686
|
+
write_all(dst, line)
|
|
687
|
+
size = chunk_size(line)
|
|
688
|
+
if size.zero?
|
|
689
|
+
relay_trailers(src, dst, buf)
|
|
690
|
+
return
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
write_all(dst, take_bytes(src, buf, size) || +"")
|
|
694
|
+
write_all(dst, take_line(src, buf) || +"")
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Trailers run to the blank line that closes them.
|
|
699
|
+
def relay_trailers(src, dst, buf)
|
|
700
|
+
loop do
|
|
701
|
+
line = take_line(src, buf)
|
|
702
|
+
return if line.nil?
|
|
703
|
+
|
|
704
|
+
write_all(dst, line)
|
|
705
|
+
return if line == CRLF
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# A chunk-size line, less the CRLF that ends it. Extensions after a
|
|
710
|
+
# semicolon are the sender's and are not ours to weigh.
|
|
711
|
+
def chunk_size(line)
|
|
712
|
+
line.split(";", 2).first.to_s.strip.to_i(16)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def take_line(sock, buf)
|
|
716
|
+
until (index = buf.index(CRLF))
|
|
717
|
+
return nil unless fill_buf(sock, buf)
|
|
718
|
+
end
|
|
719
|
+
buf.slice!(0, index + 2)
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def take_bytes(sock, buf, count)
|
|
723
|
+
out = +""
|
|
724
|
+
while out.bytesize < count
|
|
725
|
+
return nil if buf.empty? && !fill_buf(sock, buf)
|
|
726
|
+
|
|
727
|
+
take = [count - out.bytesize, buf.bytesize].min
|
|
728
|
+
out << buf.slice!(0, take)
|
|
729
|
+
end
|
|
730
|
+
out
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def fill_buf(sock, buf)
|
|
734
|
+
buf << read_chunk(sock, CHUNK_BYTES)
|
|
735
|
+
true
|
|
736
|
+
rescue EOFError
|
|
737
|
+
false
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def chunked?(headers)
|
|
741
|
+
headers["transfer-encoding"].to_s.downcase.include?("chunked")
|
|
742
|
+
end
|
|
743
|
+
|
|
655
744
|
# Block until the socket is ready for the direction a nonblocking op
|
|
656
745
|
# asked for; a timeout when the bound passes with no progress. A
|
|
657
746
|
# caller-supplied bound only changes how long we wait — what a timeout
|
data/lib/yamine/version.rb
CHANGED