Skip to content

Commit 326b5fa

Browse files
Proxy: Preserve HTTP response headers and safely rewrite HLS. v8.0.20 (#4718)
## Summary - Preserve end-to-end backend response headers for HTTP-FLV, HTTP-TS, and HLS responses. - Remove standard hop-by-hop headers and additional fields nominated by `Connection`. - Rewrite HLS segment URLs with a canonical single `&` when the original URL already has query parameters. - Repair response metadata after modifying an m3u8 body, and add wire-level regression coverage. ## Problem The HTTP proxy called `WriteHeader` before copying headers from the backend. Go commits the response at that point, so later additions such as `Content-Type`, `Cache-Control`, `ETag`, `Last-Modified`, `Vary`, and custom origin metadata never reached the downstream client. HLS playlist rewriting also generated segment URLs containing an unnecessary empty query component: ```text segment.ts?spbhid=xxx&&token=abc ``` Moving header copying before `WriteHeader` is not sufficient by itself. An m3u8 playlist is modified when the proxy inserts `spbhid`, so the backend's original `Content-Length`, strong `ETag`, digest, content encoding, and range metadata no longer describe the downstream representation. Forwarding the stale length can make Go reject the enlarged response with `http: wrote more than the declared Content-Length`. A proxy must also avoid forwarding connection-specific headers to the next hop. ## Changes - Copy backend end-to-end headers before committing HTTP-FLV, HTTP-TS, and HLS responses. - Strip `Connection`, its nominated fields, and the standard proxy/connection-specific header set. - Keep byte-transparent FLV and TS representation metadata unchanged. - Read and rewrite m3u8 playlists before sending response headers. - Recompute `Content-Length` and remove stale validators, digests, encodings, and range metadata when the playlist body changes. - Generate existing-query segment URLs as: ```text segment.ts?spbhid=xxx&token=abc ``` - Add real HTTP-boundary tests for header preservation, hop-by-hop removal, query rewriting, complete playlist delivery, and rewritten-body metadata. --------- Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
1 parent b666bd6 commit 326b5fa

5 files changed

Lines changed: 383 additions & 19 deletions

File tree

internal/proxy/http.go

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,60 @@ func buildBackendHTTPURL(ip string, port int, path string) string {
4444
return fmt.Sprintf("http://%v:%v%s", ip, port, path)
4545
}
4646

47+
// copyBackendResponseHeaders copies end-to-end response headers while dropping
48+
// fields that apply only to the backend connection. Connection can nominate
49+
// additional hop-by-hop fields, so collect those names before copying.
50+
func copyBackendResponseHeaders(dst, src http.Header) {
51+
hopByHop := map[string]bool{
52+
"Connection": true,
53+
"Proxy-Connection": true,
54+
"Keep-Alive": true,
55+
"Proxy-Authenticate": true,
56+
"Proxy-Authorization": true,
57+
"Te": true,
58+
"Trailer": true,
59+
"Transfer-Encoding": true,
60+
"Upgrade": true,
61+
}
62+
for _, value := range src.Values("Connection") {
63+
for _, name := range strings.Split(value, ",") {
64+
if name = strings.TrimSpace(name); name != "" {
65+
hopByHop[http.CanonicalHeaderKey(name)] = true
66+
}
67+
}
68+
}
69+
70+
for name, values := range src {
71+
name = http.CanonicalHeaderKey(name)
72+
if hopByHop[name] {
73+
continue
74+
}
75+
76+
dst.Del(name)
77+
for _, value := range values {
78+
dst.Add(name, value)
79+
}
80+
}
81+
}
82+
83+
// repairRewrittenResponseHeaders removes backend representation metadata that
84+
// no longer describes a rewritten response body, then publishes its new size.
85+
func repairRewrittenResponseHeaders(header http.Header, contentLength int) {
86+
for _, name := range []string{
87+
"Accept-Ranges",
88+
"Content-Digest",
89+
"Content-Encoding",
90+
"Content-MD5",
91+
"Content-Range",
92+
"Digest",
93+
"ETag",
94+
"Repr-Digest",
95+
} {
96+
header.Del(name)
97+
}
98+
header.Set("Content-Length", strconv.Itoa(contentLength))
99+
}
100+
47101
type httpStreamProxyServer struct {
48102
// The environment interface.
49103
environment env.ProxyEnvironment
@@ -347,13 +401,10 @@ func (v *httpFlvTsConnection) serveByBackend(ctx context.Context, w http.Respons
347401
return errors.Errorf("proxy stream to %v failed, status=%v", backendURL, resp.Status)
348402
}
349403

350-
// Copy all headers from backend to client.
404+
// Copy end-to-end headers before committing the response. Headers changed
405+
// after WriteHeader are not sent by net/http.
406+
copyBackendResponseHeaders(w.Header(), resp.Header)
351407
w.WriteHeader(resp.StatusCode)
352-
for k, v := range resp.Header {
353-
for _, vv := range v {
354-
w.Header().Add(k, vv)
355-
}
356-
}
357408

358409
logger.Debug(ctx, "HTTP start streaming")
359410

@@ -476,16 +527,11 @@ func (v *hlsPlayStream) serveByBackend(ctx context.Context, w http.ResponseWrite
476527
return errors.Errorf("proxy stream to %v failed, status=%v", backendURL, resp.Status)
477528
}
478529

479-
// Copy all headers from backend to client.
480-
w.WriteHeader(resp.StatusCode)
481-
for k, v := range resp.Header {
482-
for _, vv := range v {
483-
w.Header().Add(k, vv)
484-
}
485-
}
486-
487530
// For TS file, directly copy it.
488531
if !strings.HasSuffix(r.URL.Path, ".m3u8") {
532+
copyBackendResponseHeaders(w.Header(), resp.Header)
533+
w.WriteHeader(resp.StatusCode)
534+
489535
if _, err := io.Copy(w, resp.Body); err != nil {
490536
return errors.Wrapf(err, "copy stream to client, backend=%v", backendURL)
491537
}
@@ -500,13 +546,20 @@ func (v *hlsPlayStream) serveByBackend(ctx context.Context, w http.ResponseWrite
500546
return errors.Wrapf(err, "read stream from %v", backendURL)
501547
}
502548

503-
m3u8 := string(b)
549+
backendM3U8 := string(b)
550+
m3u8 := backendM3U8
504551
if strings.Contains(m3u8, ".ts?") {
505-
m3u8 = strings.ReplaceAll(m3u8, ".ts?", fmt.Sprintf(".ts?spbhid=%v&&", v.SRSProxyBackendHLSID))
552+
m3u8 = strings.ReplaceAll(m3u8, ".ts?", fmt.Sprintf(".ts?spbhid=%v&", v.SRSProxyBackendHLSID))
506553
} else {
507554
m3u8 = strings.ReplaceAll(m3u8, ".ts", fmt.Sprintf(".ts?spbhid=%v", v.SRSProxyBackendHLSID))
508555
}
509556

557+
copyBackendResponseHeaders(w.Header(), resp.Header)
558+
if m3u8 != backendM3U8 {
559+
repairRewrittenResponseHeaders(w.Header(), len(m3u8))
560+
}
561+
w.WriteHeader(resp.StatusCode)
562+
510563
if _, err := io.Copy(w, strings.NewReader(m3u8)); err != nil {
511564
return errors.Wrapf(err, "proxy m3u8 client to %v", backendURL)
512565
}

0 commit comments

Comments
 (0)