Skip to content

Commit 8b762ff

Browse files
committed
Fix silent truncation of large npm metadata responses
ReadMetadata used io.LimitReader which silently truncated responses at the size limit. For packages like drizzle-orm (~92MB metadata), this produced invalid JSON that was served to clients. Now returns ErrMetadataTooLarge when the limit is exceeded, and bumps the limit from 50MB to 100MB. Fixes #78
1 parent 94f4a7d commit 8b762ff

2 files changed

Lines changed: 30 additions & 7 deletions

File tree

internal/handler/handler.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package handler
44
import (
55
"context"
66
"database/sql"
7+
"errors"
78
"fmt"
89
"io"
910
"log/slog"
@@ -32,15 +33,26 @@ func containsPathTraversal(path string) bool {
3233

3334
const defaultHTTPTimeout = 30 * time.Second
3435

35-
// maxMetadataSize is the maximum size of upstream metadata responses (50 MB).
36+
// maxMetadataSize is the maximum size of upstream metadata responses (100 MB).
3637
// Package metadata (e.g. npm with many versions) can be large, but unbounded
3738
// reads risk OOM if an upstream misbehaves.
38-
const maxMetadataSize = 50 << 20
39+
const maxMetadataSize = 100 << 20
40+
41+
// ErrMetadataTooLarge is returned when upstream metadata exceeds maxMetadataSize.
42+
var ErrMetadataTooLarge = errors.New("metadata response exceeds size limit")
3943

4044
// ReadMetadata reads an upstream response body with a size limit to prevent OOM
41-
// from unexpectedly large responses.
45+
// from unexpectedly large responses. Returns ErrMetadataTooLarge if the response
46+
// is truncated by the limit.
4247
func ReadMetadata(r io.Reader) ([]byte, error) {
43-
return io.ReadAll(io.LimitReader(r, maxMetadataSize))
48+
data, err := io.ReadAll(io.LimitReader(r, maxMetadataSize+1))
49+
if err != nil {
50+
return nil, err
51+
}
52+
if int64(len(data)) > maxMetadataSize {
53+
return nil, ErrMetadataTooLarge
54+
}
55+
return data, nil
4456
}
4557

4658
// Proxy provides shared functionality for protocol handlers.

internal/handler/read_metadata_test.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package handler
22

33
import (
44
"bytes"
5+
"errors"
56
"testing"
67
)
78

@@ -17,9 +18,8 @@ func TestReadMetadata(t *testing.T) {
1718
}
1819
})
1920

20-
t.Run("truncates at limit", func(t *testing.T) {
21-
// Create a reader slightly larger than maxMetadataSize
22-
data := make([]byte, maxMetadataSize+100)
21+
t.Run("exactly at limit", func(t *testing.T) {
22+
data := make([]byte, maxMetadataSize)
2323
for i := range data {
2424
data[i] = 'x'
2525
}
@@ -31,4 +31,15 @@ func TestReadMetadata(t *testing.T) {
3131
t.Errorf("got length %d, want %d", len(got), maxMetadataSize)
3232
}
3333
})
34+
35+
t.Run("over limit returns error", func(t *testing.T) {
36+
data := make([]byte, maxMetadataSize+100)
37+
for i := range data {
38+
data[i] = 'x'
39+
}
40+
_, err := ReadMetadata(bytes.NewReader(data))
41+
if !errors.Is(err, ErrMetadataTooLarge) {
42+
t.Errorf("got error %v, want ErrMetadataTooLarge", err)
43+
}
44+
})
3445
}

0 commit comments

Comments
 (0)