-
-
Notifications
You must be signed in to change notification settings - Fork 169
fix: [mcp-gateway] 修复unla因未按Accept-Encoding解压 HTTP 响应引发的解析乱码问题 #234 #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,9 @@ package core | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||
| "bytes" | ||||||||||||||||||||||||||||||||||||||||
| "compress/flate" | ||||||||||||||||||||||||||||||||||||||||
| "compress/gzip" | ||||||||||||||||||||||||||||||||||||||||
| "compress/zlib" | ||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -13,6 +16,8 @@ import ( | |||||||||||||||||||||||||||||||||||||||
| "slices" | ||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| "github.com/andybalholm/brotli" | ||||||||||||||||||||||||||||||||||||||||
| "github.com/klauspost/compress/zstd" | ||||||||||||||||||||||||||||||||||||||||
| "github.com/gin-gonic/gin" | ||||||||||||||||||||||||||||||||||||||||
| "go.uber.org/zap" | ||||||||||||||||||||||||||||||||||||||||
| "golang.org/x/net/proxy" | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,6 +35,101 @@ import ( | |||||||||||||||||||||||||||||||||||||||
| "github.com/amoylab/unla/pkg/mcp" | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| func parseContentEncodings(contentEncoding string) []string { | ||||||||||||||||||||||||||||||||||||||||
| parts := strings.Split(contentEncoding, ",") | ||||||||||||||||||||||||||||||||||||||||
| encodings := make([]string, 0, len(parts)) | ||||||||||||||||||||||||||||||||||||||||
| for _, part := range parts { | ||||||||||||||||||||||||||||||||||||||||
| encoding := strings.ToLower(strings.TrimSpace(part)) | ||||||||||||||||||||||||||||||||||||||||
| if encoding == "" || encoding == "identity" { | ||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| encodings = append(encodings, encoding) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return encodings | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| func decodeBodyBytesByEncoding(body []byte, encoding string) ([]byte, error) { | ||||||||||||||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||||||||||||||
| reader io.Reader = bytes.NewReader(body) | ||||||||||||||||||||||||||||||||||||||||
| closeFn func() error | ||||||||||||||||||||||||||||||||||||||||
| decodeError error | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| switch encoding { | ||||||||||||||||||||||||||||||||||||||||
| case "gzip", "x-gzip": | ||||||||||||||||||||||||||||||||||||||||
| gzReader, err := gzip.NewReader(reader) | ||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to create gzip reader: %w", err) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| reader = gzReader | ||||||||||||||||||||||||||||||||||||||||
| closeFn = gzReader.Close | ||||||||||||||||||||||||||||||||||||||||
| case "br": | ||||||||||||||||||||||||||||||||||||||||
| reader = brotli.NewReader(reader) | ||||||||||||||||||||||||||||||||||||||||
| case "zstd": | ||||||||||||||||||||||||||||||||||||||||
| zstdReader, err := zstd.NewReader(reader) | ||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to create zstd reader: %w", err) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| reader = zstdReader | ||||||||||||||||||||||||||||||||||||||||
| closeFn = func() error { | ||||||||||||||||||||||||||||||||||||||||
| zstdReader.Close() | ||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+68
to
+77
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): Preserve and surface errors from zstdReader.Close instead of discarding them In the case "zstd":
zstdReader, err := zstd.NewReader(reader)
if err != nil {
return nil, fmt.Errorf("failed to create zstd reader: %w", err)
}
reader = zstdReader
closeFn = func() error {
return zstdReader.Close()
}This keeps error handling consistent and ensures zstd close errors are observable to callers.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| case "deflate": | ||||||||||||||||||||||||||||||||||||||||
| zlibReader, err := zlib.NewReader(reader) | ||||||||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||||||||
| reader = zlibReader | ||||||||||||||||||||||||||||||||||||||||
| closeFn = zlibReader.Close | ||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| // Some servers send raw deflate stream for deflate token. | ||||||||||||||||||||||||||||||||||||||||
| flateReader := flate.NewReader(bytes.NewReader(body)) | ||||||||||||||||||||||||||||||||||||||||
| reader = flateReader | ||||||||||||||||||||||||||||||||||||||||
| closeFn = flateReader.Close | ||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("unsupported content encoding: %s", encoding) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| decoded, err := io.ReadAll(reader) | ||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||
| decodeError = fmt.Errorf("failed to decode %s body: %w", encoding, err) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if closeFn != nil { | ||||||||||||||||||||||||||||||||||||||||
| closeErr := closeFn() | ||||||||||||||||||||||||||||||||||||||||
| if decodeError != nil { | ||||||||||||||||||||||||||||||||||||||||
| return nil, decodeError | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if closeErr != nil { | ||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to close %s decoder: %w", encoding, closeErr) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if decodeError != nil { | ||||||||||||||||||||||||||||||||||||||||
| return nil, decodeError | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return decoded, nil | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| func readDecodedResponseBody(resp *http.Response) ([]byte, error) { | ||||||||||||||||||||||||||||||||||||||||
| if resp == nil || resp.Body == nil { | ||||||||||||||||||||||||||||||||||||||||
| return nil, nil | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| body, err := io.ReadAll(resp.Body) | ||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to read response body: %w", err) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| encodings := parseContentEncodings(resp.Header.Get("Content-Encoding")) | ||||||||||||||||||||||||||||||||||||||||
| for i := len(encodings) - 1; i >= 0; i-- { | ||||||||||||||||||||||||||||||||||||||||
| body, err = decodeBodyBytesByEncoding(body, encodings[i]) | ||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return body, nil | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // shouldIgnoreHeader checks if a header should be ignored based on configuration | ||||||||||||||||||||||||||||||||||||||||
| func (s *Server) shouldIgnoreHeader(headerName string) bool { | ||||||||||||||||||||||||||||||||||||||||
| // If forward is disabled, don't ignore any headers (backward compatibility) | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -330,8 +430,8 @@ func (s *Server) executeHTTPTool(c *gin.Context, conn session.Connection, tool * | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| defer resp.Body.Close() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Read response body for logging in case of error | ||||||||||||||||||||||||||||||||||||||||
| respBodyBytes, err := io.ReadAll(resp.Body) | ||||||||||||||||||||||||||||||||||||||||
| // Read and decode response body for logging and unified downstream processing. | ||||||||||||||||||||||||||||||||||||||||
| respBodyBytes, err := readDecodedResponseBody(resp) | ||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||
| logger.Error("failed to read response body", | ||||||||||||||||||||||||||||||||||||||||
| zap.String("tool", tool.Name), | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -341,8 +441,10 @@ func (s *Server) executeHTTPTool(c *gin.Context, conn session.Connection, tool * | |||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to read response body: %w", err) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Restore response body for further processing | ||||||||||||||||||||||||||||||||||||||||
| // Restore decoded response body for further processing. | ||||||||||||||||||||||||||||||||||||||||
| resp.Body = io.NopCloser(bytes.NewBuffer(respBodyBytes)) | ||||||||||||||||||||||||||||||||||||||||
| resp.Header.Del("Content-Encoding") | ||||||||||||||||||||||||||||||||||||||||
| resp.Header.Del("Content-Length") | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+444
to
+447
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): Consider updating resp.ContentLength alongside the Content-Length header after decoding Right now the resp.ContentLength = int64(len(respBodyBytes)) // or -1 if you want it treated as unknown
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Log response status | ||||||||||||||||||||||||||||||||||||||||
| logger.Debug("received HTTP response", | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Add coverage for multiple Content-Encoding values and encoding order in readDecodedResponseBody
Since readDecodedResponseBody handles multiple encodings by iterating them in reverse, please add at least one test with a chained Content-Encoding (e.g.
gzip, brand/orbr, gzip) to verify that: (1) decoding happens in the correct order, and (2) errors from any step in the chain are surfaced. This will validate the reverse-iteration logic and protect against regressions for multi-encoding responses.Suggested implementation:
These tests assume that
readDecodedResponseBodytreats"deflate"as zlib-wrapped data and that it iterates the encodings slice in reverse order, as suggested by your comment. If the implementation instead:compress/flatedirectly for"deflate", or"gzip, deflate"rather than[]string{"gzip", "deflate"},you should adjust:
encodeWithGzipThenZlib) to match the actual"deflate"implementation (e.g., usingflate.NewWriterinstead ofzlib.NewWriter), and/orContent-Encodingheader construction to mirror how your code reads/normalizes multiple encodings (e.g.,Header: http.Header{"Content-Encoding": []string{"gzip, deflate"}}).