diff --git a/upload_url.go b/upload_url.go index 0519dbb..e2b1df5 100644 --- a/upload_url.go +++ b/upload_url.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "log/slog" + "mime" "net/http" "net/url" "strings" @@ -259,9 +260,18 @@ func (a *App) uploadDirectHandler(w http.ResponseWriter, r *http.Request) { jsonFail(w, "upload url expired", http.StatusOK) return } - if g.ContentType != "" && r.Header.Get("Content-Type") != g.ContentType { - jsonFail(w, "content type mismatch", http.StatusOK) - return + if g.ContentType != "" { + // Compare only the media type (type/subtype), ignoring parameters such as + // charset/boundary per RFC 7231 §3.1.1.1: a client sending + // "application/json; charset=utf-8" against a grant for "application/json" + // is the same media type. ParseMediaType also lowercases, so the match is + // case-insensitive; a malformed/empty header parses to "" and is rejected. + got, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + want, _, _ := mime.ParseMediaType(g.ContentType) + if got != want { + jsonFail(w, "content type mismatch", http.StatusOK) + return + } } if r.Body == nil { jsonFail(w, "body empty", http.StatusOK) diff --git a/upload_url_test.go b/upload_url_test.go index ca8f8ba..3f89533 100644 --- a/upload_url_test.go +++ b/upload_url_test.go @@ -470,6 +470,29 @@ func TestUploadDirect_ContentTypeMismatch(t *testing.T) { } } +// TestUploadDirect_ContentTypeAllowsParams guards that a charset/boundary +// parameter on the granted media type is accepted — the content-type check +// compares media types (type/subtype) only, not the raw header. +func TestUploadDirect_ContentTypeAllowsParams(t *testing.T) { + t.Parallel() + db := newTestDB(t) + bkt := newTestBucket(t) + app := newTestApp(bkt, authorized) + + resp := issueUploadURL(t, app, db, uploadURLRequest{Project: "p", ContentType: "application/pdf", MaxSize: 100}) + token := uploadTokenOf(t, resp.Result.UploadURL) + + w := doPut(t, app, db, token, "application/pdf; charset=utf-8", "data") + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200 (%s)", w.Code, w.Body.String()) + } + var pr putResp + json.NewDecoder(w.Body).Decode(&pr) + if !pr.OK { + t.Fatalf("expected ok=true (charset param on granted media type accepted), got %s", w.Body.String()) + } +} + func TestUploadDirect_InvalidToken(t *testing.T) { t.Parallel() db := newTestDB(t)