Skip to content

Commit e1885b7

Browse files
authored
feat(http)!: query params take precedence over Accept header (IPIP-523) (#62)
The format query parameter now takes precedence over the Accept header for content type negotiation. CAR option query parameters (car-order, car-dups, car-version) also take precedence over the corresponding parameters in the Accept header.
1 parent 2d2e657 commit e1885b7

2 files changed

Lines changed: 76 additions & 46 deletions

File tree

http/parse.go

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -100,79 +100,98 @@ func ParseFilename(req *http.Request, accepts []ContentType) (string, error) {
100100
// parameters
101101
// https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
102102
//
103-
// Per the spec: "When both Accept HTTP header and format query parameter are
104-
// present, Accept SHOULD take precedence." However, wildcard Accept headers
105-
// (*/* and application/*) are treated as having no format preference, allowing
106-
// the format parameter to be used instead.
103+
// Per IPIP-523: the format query parameter takes precedence over the Accept
104+
// header. CAR option query parameters (car-version, car-order, car-dups) also
105+
// take precedence over the corresponding parameters in the Accept header.
107106
func CheckFormat(req *http.Request) ([]ContentType, error) {
108-
format := req.URL.Query().Get("format")
107+
query := req.URL.Query()
108+
109+
format := query.Get("format")
109110
switch format {
110111
case "", FormatParameterCar, FormatParameterRaw:
111112
default:
112113
return nil, fmt.Errorf("invalid format parameter; unsupported: %q", format)
113114
}
114115

116+
// Validate CAR option query parameters (IPIP-523)
117+
carOrder := query.Get("car-order")
118+
if carOrder != "" {
119+
switch carOrder {
120+
case "dfs", "unk":
121+
default:
122+
return nil, fmt.Errorf("invalid car-order parameter; unsupported: %q", carOrder)
123+
}
124+
}
125+
carDups := query.Get("car-dups")
126+
if carDups != "" {
127+
switch carDups {
128+
case "y", "n":
129+
default:
130+
return nil, fmt.Errorf("invalid car-dups parameter; unsupported: %q", carDups)
131+
}
132+
}
133+
carVersion := query.Get("car-version")
134+
if carVersion != "" && carVersion != MimeTypeCarVersion {
135+
return nil, fmt.Errorf("invalid car-version parameter; unsupported: %q", carVersion)
136+
}
137+
115138
accept := req.Header.Get("Accept")
116139

117140
// Parse Accept header if present
118141
var accepts []ContentType
119142
if accept != "" {
120143
accepts = ParseAccept(accept)
121-
if len(accepts) == 0 {
122-
// Invalid Accept header - if we have a format parameter, use it
123-
if format != "" {
124-
switch format {
125-
case FormatParameterCar:
126-
return []ContentType{DefaultContentType().WithMimeType(MimeTypeCar)}, nil
127-
case FormatParameterRaw:
128-
return []ContentType{DefaultContentType().WithMimeType(MimeTypeRaw)}, nil
129-
}
130-
}
144+
if len(accepts) == 0 && format == "" {
131145
return nil, fmt.Errorf("invalid Accept header; unsupported: %q", accept)
132146
}
133147
}
134148

135-
// Check if Accept is a wildcard (essentially no preference)
136-
hasWildcardAccept := false
137-
if len(accepts) > 0 {
138-
// If the highest priority Accept is a wildcard, treat as no preference
139-
if accepts[0].MimeType == "*/*" || accepts[0].MimeType == "application/*" {
140-
hasWildcardAccept = true
141-
}
142-
}
143-
144-
// Spec says Accept should take precedence over format parameter
145-
// However, wildcards are treated as no preference
146-
if len(accepts) > 0 && !hasWildcardAccept {
147-
// Specific Accept header takes precedence
148-
return accepts, nil
149-
}
149+
var result []ContentType
150150

151-
// No specific Accept preference (either no Accept, invalid Accept with format, or wildcard)
152-
// Use format parameter if present
151+
// Per IPIP-523: format query parameter takes precedence over Accept header
153152
if format != "" {
154153
switch format {
155154
case FormatParameterCar:
156-
// If we have CAR accepts (even wildcards), try to inherit parameters
155+
ct := DefaultContentType()
156+
// If Accept also specifies CAR, inherit its params as defaults
157157
for _, a := range accepts {
158-
if a.IsCar() {
159-
return []ContentType{a.WithMimeType(MimeTypeCar)}, nil
158+
if a.MimeType == MimeTypeCar {
159+
ct = a.WithQuality(1)
160+
break
160161
}
161162
}
162-
return []ContentType{DefaultContentType().WithMimeType(MimeTypeCar)}, nil
163+
result = []ContentType{ct}
163164
case FormatParameterRaw:
164-
return []ContentType{DefaultContentType().WithMimeType(MimeTypeRaw)}, nil
165+
result = []ContentType{DefaultContentType().WithMimeType(MimeTypeRaw)}
165166
}
167+
} else if len(accepts) > 0 {
168+
result = accepts
169+
} else {
170+
return nil, fmt.Errorf("neither a valid Accept header nor format parameter were provided")
166171
}
167172

168-
// Wildcard Accept with no format parameter - return the wildcard accepts
169-
// This allows the caller to convert wildcards to a sensible default
170-
// (typically CAR format)
171-
if len(accepts) > 0 {
172-
return accepts, nil
173+
// Per IPIP-523: CAR option query parameters take precedence over
174+
// parameters in the Accept header
175+
if carOrder != "" || carDups != "" {
176+
for i := range result {
177+
if result[i].MimeType == MimeTypeCar {
178+
switch carOrder {
179+
case "dfs":
180+
result[i].Order = ContentTypeOrderDfs
181+
case "unk":
182+
result[i].Order = ContentTypeOrderUnk
183+
}
184+
switch carDups {
185+
case "y":
186+
result[i].Duplicates = true
187+
case "n":
188+
result[i].Duplicates = false
189+
}
190+
}
191+
}
173192
}
174193

175-
return nil, fmt.Errorf("neither a valid Accept header nor format parameter were provided")
194+
return result, nil
176195
}
177196

178197
// ParseAccept validates a request Accept header and returns whether or not

http/parse_test.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,26 @@ func TestCheckFormat(t *testing.T) {
133133
{"accept no dups", "application/vnd.ipld.car; dups=n", "", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithDuplicates(false)}, ""},
134134
{"accept no dups and cruft", "application/vnd.ipld.car; dups=n; bip; bop", "", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithDuplicates(false)}, ""},
135135
{"valid accept but format=bop (err)", "application/vnd.ipld.car; dups=y", "format=bop", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, "invalid format parameter; unsupported: \"bop\""},
136-
{"specific accept car with format=car (accept wins per spec)", "application/vnd.ipld.car; dups=y", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
137-
{"specific accept car with format=raw (accept wins per spec)", "application/vnd.ipld.car; dups=n", "format=raw", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithDuplicates(false)}, ""},
138-
{"specific accept raw with format=car (accept wins per spec)", "application/vnd.ipld.raw", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithMimeType(trustlesshttp.MimeTypeRaw)}, ""},
136+
// IPIP-523: format query parameter takes precedence over Accept header
137+
{"format=car with car Accept (format wins, inherits car params)", "application/vnd.ipld.car; dups=y", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
138+
{"format=raw with car Accept (format wins)", "application/vnd.ipld.car; dups=n", "format=raw", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithMimeType(trustlesshttp.MimeTypeRaw)}, ""},
139+
{"format=car with raw Accept (format wins)", "application/vnd.ipld.raw", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
140+
{"format=car inherits dups=n from Accept", "application/vnd.ipld.car; dups=n", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithDuplicates(false)}, ""},
141+
{"format=car inherits order=unk from Accept", "application/vnd.ipld.car; order=unk", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithOrder(trustlesshttp.ContentTypeOrderUnk)}, ""},
139142
{"invalid accept but format=car (format wins)", "application/vnd.ipld.car; dups=YES!", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
140143
{"invalid accept but format=raw (format wins)", "application/vnd.ipld.car; dups=YES!", "format=raw", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithMimeType(trustlesshttp.MimeTypeRaw)}, ""},
141144
{"wildcard */* with format=raw (format wins)", "*/*", "format=raw", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithMimeType(trustlesshttp.MimeTypeRaw)}, ""},
142145
{"wildcard */* with format=car (format wins)", "*/*", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
143146
{"wildcard application/* with format=raw (format wins)", "application/*", "format=raw", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithMimeType(trustlesshttp.MimeTypeRaw)}, ""},
144147
{"wildcard application/* with format=car (format wins)", "application/*", "format=car", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
148+
// IPIP-523: CAR option query parameters take precedence over Accept header params
149+
{"car-order=dfs overrides Accept order=unk", "application/vnd.ipld.car; order=unk", "car-order=dfs", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
150+
{"car-dups=n overrides Accept dups=y", "application/vnd.ipld.car; dups=y", "car-dups=n", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithDuplicates(false)}, ""},
151+
{"car-order=dfs with format=car overrides Accept order=unk", "application/vnd.ipld.car; order=unk", "format=car&car-order=dfs", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
152+
{"car-dups=y with format=car overrides Accept dups=n", "application/vnd.ipld.car; dups=n", "format=car&car-dups=y", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType()}, ""},
153+
{"car-order=bork (err)", "application/vnd.ipld.car", "car-order=bork", nil, "invalid car-order parameter; unsupported: \"bork\""},
154+
{"car-dups=bork (err)", "application/vnd.ipld.car", "car-dups=bork", nil, "invalid car-dups parameter; unsupported: \"bork\""},
155+
{"car-version=2 (err)", "application/vnd.ipld.car", "car-version=2", nil, "invalid car-version parameter; unsupported: \"2\""},
145156
{"ordered, valid", "application/vnd.ipld.raw, application/*, application/vnd.ipld.car; dups=y", "", []trustlesshttp.ContentType{trustlesshttp.DefaultContentType().WithMimeType(trustlesshttp.MimeTypeRaw), trustlesshttp.DefaultContentType().WithMimeType("application/*"), trustlesshttp.DefaultContentType().WithDuplicates(true)}, ""},
146157
} {
147158
t.Run(tc.name, func(t *testing.T) {

0 commit comments

Comments
 (0)