Unverified Commit b583a2f1 authored by david-perez's avatar david-perez Committed by GitHub
Browse files

Fix request `Content-Type` header checking in servers (#3690)

This fixes two bugs:

1. `Content-Type` header checking was succeeding when no `Content-Type`
   header was present but one was expected.
2. When a shape was `@httpPayload`-bound, `Content-Type` header checking
   occurred even when no payload was being sent. In this case it is not
   necessary to check the header, since there is no content.

Code has been refactored and cleaned up. The crux of the logic is now
easier to understand, and contained in `content_type_header_classifier`.

## Checklist
<!--- If a checkbox below is not applicable, then please DELETE it
rather than leaving it unchecked -->
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the
smithy-rs codegen or runtime crates

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
parent a415cfef
Loading
Loading
Loading
Loading
+14 −1
Original line number Diff line number Diff line
@@ -10,3 +10,16 @@
# references = ["smithy-rs#920"]
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
# author = "rcoh"
[[smithy-rs]]
message = """Fix request `Content-Type` header checking

Two bugs related to how servers were checking the `Content-Type` header in incoming requests have been fixed:

1. `Content-Type` header checking was incorrectly succeeding when no `Content-Type` header was present but one was expected.
2. When a shape was @httpPayload`-bound, `Content-Type` header checking occurred even when no payload was being sent. In this case it is not necessary to check the header, since there is no content.

This is a breaking change in that servers are now stricter at enforcing the expected `Content-Type` header is being sent by the client in general, and laxer when the shape is bound with `@httpPayload`.
"""
references = ["smithy-rs#3690"]
meta = { "breaking" = true, "tada" = false, "bug" = true, "target" = "server"}
author = "david-perez"
+10 −1
Original line number Diff line number Diff line
@@ -67,7 +67,16 @@ val allCodegenTests = listOf(
    ClientTest(
        "aws.protocoltests.restjson#RestJsonExtras",
        "rest_json_extras",
        dependsOn = listOf("rest-json-extras.smithy"),
        dependsOn = listOf(
            "rest-json-extras.smithy",
            // TODO(https://github.com/smithy-lang/smithy/pull/2310): Can be deleted when consumed in next Smithy version.
            "rest-json-extras-2310.smithy",
            // TODO(https://github.com/smithy-lang/smithy/pull/2314): Can be deleted when consumed in next Smithy version.
            "rest-json-extras-2314.smithy",
            // TODO(https://github.com/smithy-lang/smithy/pull/2315): Can be deleted when consumed in next Smithy version.
            // TODO(https://github.com/smithy-lang/smithy/pull/2331): Can be deleted when consumed in next Smithy version.
            "rest-json-extras-2315.smithy",
        ),
    ),
    ClientTest("aws.protocoltests.misc#MiscService", "misc", dependsOn = listOf("misc.smithy")),
    ClientTest("aws.protocoltests.restxml#RestXml", "rest_xml", addMessageToErrors = false),
+35 −0
Original line number Diff line number Diff line
$version: "1.0"

namespace aws.protocoltests.restjson

use aws.protocols#restJson1
use smithy.test#httpMalformedRequestTests

@http(method: "POST", uri: "/MalformedContentTypeWithBody")
operation MalformedContentTypeWithBody2 {
    input: GreetingStruct
}

structure GreetingStruct {
    salutation: String,
}

apply MalformedContentTypeWithBody2 @httpMalformedRequestTests([
    {
        id: "RestJsonWithBodyExpectsApplicationJsonContentTypeNoHeaders",
        documentation: "When there is modeled input, the content type must be application/json",
        protocol: restJson1,
        request: {
            method: "POST",
            uri: "/MalformedContentTypeWithBody",
            body: "{}",
        },
        response: {
            code: 415,
            headers: {
                "x-amzn-errortype": "UnsupportedMediaTypeException"
            }
        },
        tags: [ "content-type" ]
    }
])
+39 −0
Original line number Diff line number Diff line
$version: "1.0"

namespace aws.protocoltests.restjson

use aws.protocols#restJson1
use smithy.test#httpRequestTests

/// This example serializes a blob shape in the payload.
///
/// In this example, no JSON document is synthesized because the payload is
/// not a structure or a union type.
@http(uri: "/HttpPayloadTraits", method: "POST")
operation HttpPayloadTraits2 {
    input: HttpPayloadTraitsInputOutput,
    output: HttpPayloadTraitsInputOutput
}

apply HttpPayloadTraits2 @httpRequestTests([
    {
        id: "RestJsonHttpPayloadTraitsWithBlobAcceptsNoContentType",
        documentation: """
            Servers must accept no content type for blob inputs
            without the media type trait.""",
        protocol: restJson1,
        method: "POST",
        uri: "/HttpPayloadTraits",
        body: "This is definitely a jpeg",
        bodyMediaType: "application/octet-stream",
        headers: {
            "X-Foo": "Foo",
        },
        params: {
            foo: "Foo",
            blob: "This is definitely a jpeg"
        },
        appliesTo: "server",
        tags: [ "content-type" ]
    }
])
+133 −0
Original line number Diff line number Diff line
$version: "2.0"

namespace aws.protocoltests.restjson

use smithy.test#httpRequestTests
use smithy.test#httpResponseTests
use smithy.test#httpMalformedRequestTests
use smithy.framework#ValidationException

@http(uri: "/EnumPayload2", method: "POST")
@httpRequestTests([
    {
        id: "RestJsonEnumPayloadRequest2",
        uri: "/EnumPayload2",
        headers: { "Content-Type": "text/plain" },
        body: "enumvalue",
        params: { payload: "enumvalue" },
        method: "POST",
        protocol: "aws.protocols#restJson1"
    }
])
@httpResponseTests([
    {
        id: "RestJsonEnumPayloadResponse2",
        headers: { "Content-Type": "text/plain" },
        body: "enumvalue",
        params: { payload: "enumvalue" },
        protocol: "aws.protocols#restJson1",
        code: 200
    }
])
operation HttpEnumPayload2 {
    input: EnumPayloadInput,
    output: EnumPayloadInput
    errors: [ValidationException]
}

@http(uri: "/StringPayload2", method: "POST")
@httpRequestTests([
    {
        id: "RestJsonStringPayloadRequest2",
        uri: "/StringPayload2",
        body: "rawstring",
        bodyMediaType: "text/plain",
        headers: {
            "Content-Type": "text/plain",
        },
        requireHeaders: [
            "Content-Length"
        ],
        params: { payload: "rawstring" },
        method: "POST",
        protocol: "aws.protocols#restJson1"
    }
])
@httpResponseTests([
    {
        id: "RestJsonStringPayloadResponse2",
        headers: { "Content-Type": "text/plain" },
        body: "rawstring",
        bodyMediaType: "text/plain",
        params: { payload: "rawstring" },
        protocol: "aws.protocols#restJson1",
        code: 200
    }
])
@httpMalformedRequestTests([
    {
        id: "RestJsonStringPayloadNoContentType2",
        documentation: "Serializes a string in the HTTP payload without a content-type header",
        protocol: "aws.protocols#restJson1",
        request: {
            method: "POST",
            uri: "/StringPayload2",
            body: "rawstring",
            // We expect a `Content-Type` header but none was provided.
        },
        response: {
            code: 415,
            headers: {
                "x-amzn-errortype": "UnsupportedMediaTypeException"
            }
        },
        tags: [ "content-type" ]
    },
    {
        id: "RestJsonStringPayloadWrongContentType2",
        documentation: "Serializes a string in the HTTP payload without the expected content-type header",
        protocol: "aws.protocols#restJson1",
        request: {
            method: "POST",
            uri: "/StringPayload2",
            body: "rawstring",
            headers: {
                // We expect `text/plain`.
                "Content-Type": "application/json",
            },
        },
        response: {
            code: 415,
            headers: {
                "x-amzn-errortype": "UnsupportedMediaTypeException"
            }
        },
        tags: [ "content-type" ]
    },
    {
        id: "RestJsonStringPayloadUnsatisfiableAccept2",
        documentation: "Serializes a string in the HTTP payload with an unstatisfiable accept header",
        protocol: "aws.protocols#restJson1",
        request: {
            method: "POST",
            uri: "/StringPayload2",
            body: "rawstring",
            headers: {
                "Content-Type": "text/plain",
                // We can't satisfy this requirement; the server will return `text/plain`.
                "Accept": "application/json",
            },
        },
        response: {
            code: 406,
            headers: {
                "x-amzn-errortype": "NotAcceptableException"
            }
        },
        tags: [ "accept" ]
    },
])
operation HttpStringPayload2 {
    input: StringPayloadInput,
    output: StringPayloadInput
}
Loading