Repository: smithy-lang/smithy
Pull request #2297 (2024-05-23 00:52:39)

Clarify RPCv2 response event stream behavior
#### Background
* What do these changes do? 
Clarify when an `Accept` header is needed for Smithy RPCv2 response event streams.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/docs/source-1.0/spec/aws/aws-json.rst.template b/docs/source-1.0/spec/aws/aws-json.rst.template
index ed8744312..641970472 100644
--- a/docs/source-1.0/spec/aws/aws-json.rst.template
+++ b/docs/source-1.0/spec/aws/aws-json.rst.template
@@ -50,7 +50,7 @@ The |quoted shape name| protocol uses the following headers:
     * - ``Content-Length``
       - true
       - The standard ``Content-Length`` header defined by
-        `RFC 7230 Section 3.3.2`_.
+        :rfc:`9110#section-8.6`.
     * - ``X-Amz-Target``
       - true for requests
       - The value of this header is the :token:`shape name <smithy:Identifier>` of the
@@ -207,5 +207,3 @@ These compliance tests define a model that is used to define test cases and
 the expected serialized HTTP requests and responses for each case.
 
 *TODO: Add event stream handling specifications.*
-
-.. _`RFC 7230 Section 3.3.2`: https://tools.ietf.org/html/rfc7230#section-3.3.2
diff --git a/docs/source-1.0/spec/aws/aws-query-serialization.rst.template b/docs/source-1.0/spec/aws/aws-query-serialization.rst.template
index 4b11f1fbb..fac8711b5 100644
--- a/docs/source-1.0/spec/aws/aws-query-serialization.rst.template
+++ b/docs/source-1.0/spec/aws/aws-query-serialization.rst.template
@@ -97,5 +97,3 @@ to the input's key.
     * - ``union``
       - A union is serialized identically to a ``structure`` shape, but only a
         single member can be set to a non-null value.
-
-.. _`RFC 7230 Section 3.3.2`: https://tools.ietf.org/html/rfc7230#section-3.3.2
diff --git a/docs/source-1.0/spec/core/http-traits.rst b/docs/source-1.0/spec/core/http-traits.rst
index 61d2368e9..2ae3190bf 100644
--- a/docs/source-1.0/spec/core/http-traits.rst
+++ b/docs/source-1.0/spec/core/http-traits.rst
@@ -14,8 +14,8 @@ and error structures are considered when serializing HTTP messages.
 
 .. important::
 
-    Violating `HTTP specifications`_ or relying on poorly-supported HTTP
-    functionality when defining HTTP bindings will limit interoperability
+    Violating :rfc:`HTTP specifications <9110>` or relying on poorly-supported
+    HTTP functionality when defining HTTP bindings will limit interoperability
     and likely lead to undefined behavior across Smithy implementations. For
     example, avoid defining GET/DELETE requests with payloads, defining
     response payloads for operations with a 204/205 status, etc.
@@ -106,9 +106,8 @@ method
 The ``method`` property defines the HTTP method of the operation (e.g., "GET",
 "PUT", "POST", "DELETE", "PATCH", etc). Smithy will use this value literally
 and will perform no validation on the method. The ``method`` value SHOULD
-match the ``operation`` production rule of :rfc:`7230#appendix-B`. This
-property does not influence the safety or idempotency characteristics of an
-operation.
+match one of the definitions found in :rfc:`9110#section-9.3`. This property
+does not influence the safety or idempotency characteristics of an operation.
 
 
 .. _http-uri:
@@ -117,7 +116,7 @@ uri
 ---
 
 The ``uri`` property defines the *request-target* of the operation in
-*origin-form* as defined in :rfc:`7230#section-5.3.1`. The URI is a simple
+*origin-form* as defined in :rfc:`9112#section-3.2.1`. The URI is a simple
 pattern that Smithy uses to match HTTP requests to operations and to bind
 components of the request URI to fields in the operations's input structure.
 :dfn:`Patterns` consist of literal characters that MUST be matched in the
@@ -525,9 +524,8 @@ Trait selector
     ``structure`` member that targets a list of these types.
 Value type
     ``string`` value defining a valid HTTP header field name according to
-    :rfc:`section 3.2 of RFC7230 <7230#section-3.2>`. The value MUST NOT be
-    empty and MUST be case-insensitively unique across all other members of
-    the structure.
+    :rfc:`9110#section-5.1`. The value MUST NOT be empty and MUST be
+    case-insensitively unique across all other members of the structure.
 Conflicts with
    :ref:`httpLabel-trait`,
    :ref:`httpQuery-trait`,
@@ -546,7 +544,7 @@ Conflicts with
 * ``string`` values with a :ref:`mediaType-trait` are always base64 encoded.
 * ``timestamp`` values are serialized using the ``http-date``
   format by default, as defined in the ``IMF-fixdate`` production of
-  :rfc:`7231#section-7.1.1.1`. The :ref:`timestampFormat-trait` MAY be used
+  :rfc:`9110#section-5.6.7`. The :ref:`timestampFormat-trait` MAY be used
   to use a custom serialization format.
 
 .. rubric:: Do not put too much data in HTTP headers
@@ -954,10 +952,10 @@ the body of the response.
 
 .. rubric:: Do not put too much data in the query string
 
-While there is no limit placed on the length of an `HTTP request line`_,
-many HTTP client and server implementations enforce limits in practice.
-Carefully consider the maximum allowed length of each member that is bound to
-an HTTP query string or path.
+While there is no limit placed on the length of an
+:rfc:`HTTP request line <9112#section-3>`, many HTTP client and server
+implementations enforce limits in practice. Carefully consider the maximum
+allowed length of each member that is bound to an HTTP query string or path.
 
 
 .. smithy-trait:: smithy.api#httpQueryParams
@@ -1335,5 +1333,3 @@ marked with the ``httpPayload`` trait:
 
 
 .. _percent-encoded: https://tools.ietf.org/html/rfc3986#section-2.1
-.. _HTTP request line: https://tools.ietf.org/html/rfc7230.html#section-3.1.1
-.. _HTTP specifications: https://datatracker.ietf.org/doc/html/rfc7230
diff --git a/docs/source-1.0/spec/core/model.rst b/docs/source-1.0/spec/core/model.rst
index bd5c4e6ae..78f1659dc 100644
--- a/docs/source-1.0/spec/core/model.rst
+++ b/docs/source-1.0/spec/core/model.rst
@@ -2020,7 +2020,7 @@ The following example defines the ``PutForecast`` operation.
 .. rubric:: Put semantics
 
 The semantics of a ``put`` lifecycle operation are similar to the semantics
-of a HTTP PUT method as described in :rfc:`section 4.3.4 of [RFC7231] <7231#section-4.3.4>`:
+of a HTTP PUT method as described in :rfc:`section 4.3.4 of [RFC9110] <9110#section-9.3.4>`:
 
   The PUT method requests that the state of the target resource be
   created or replaced ...
diff --git a/docs/source-1.0/spec/core/protocol-traits.rst b/docs/source-1.0/spec/core/protocol-traits.rst
index e420498b0..61c1af42b 100644
--- a/docs/source-1.0/spec/core/protocol-traits.rst
+++ b/docs/source-1.0/spec/core/protocol-traits.rst
@@ -309,7 +309,7 @@ Smithy defines the following built-in timestamp formats:
         values that contain offsets gracefully by normalizing them to UTC.
     * - http-date
       - An HTTP date as defined by the ``IMF-fixdate`` production in
-        :rfc:`7231#section-7.1.1.1` (for example,
+        :rfc:`9110#section-5.6.7` (for example,
         ``Tue, 29 Apr 2014 18:30:38 GMT``). A deserializer that encounters an
         ``http-date`` timestamp with fractional precision SHOULD fail to
         deserialize the value (for example, an HTTP server SHOULD return a 400
diff --git a/docs/source-2.0/additional-specs/protocols/smithy-rpc-v2.rst b/docs/source-2.0/additional-specs/protocols/smithy-rpc-v2.rst
index 40479383a..f4abe4669 100644
--- a/docs/source-2.0/additional-specs/protocols/smithy-rpc-v2.rst
+++ b/docs/source-2.0/additional-specs/protocols/smithy-rpc-v2.rst
@@ -214,10 +214,10 @@ Buffered RPC requests for the ``rpcv2Cbor`` protocol SHOULD include a
 ``Content-Length`` header. Event streaming requests MUST NOT specify a content
 length (instead using ``Transfer-Encoding: chunked`` on HTTP/1.1).
 
-Event streaming requests for the ``rpcv2Cbor`` protocol MUST include an
-``Accept`` header set to the value ``application/vnd.amazon.eventstream``.
-Other forms of content streaming MAY be added in the future, utilizing
-different values for ``Accept``.
+Requests with event stream responses for the ``rpcv2Cbor`` protocol MUST
+include an ``Accept`` header set to the value
+``application/vnd.amazon.eventstream``. Other forms of content streaming MAY be
+added in the future, utilizing different values for ``Accept``.
 
 In summary, the ``rpcv2Cbor`` protocol defines behavior for the following
 headers for requests:
@@ -238,11 +238,12 @@ headers for requests:
         is ``application/vnd.amazon.eventstream``.
     * - ``Content-Length``
       - Conditional
-      - The standard ``Content-Length`` header defined by :rfc:`7230#section-3.3.2`.
+      - The standard ``Content-Length`` header defined by :rfc:`9110#section-8.6`.
         For event streaming requests, this MUST NOT be set.
     * - ``Accept``
       - Conditional
-      - For event streaming requests, to the value ``application/vnd.amazon.eventstream``.
+      - For requests with event streaming responses, this is
+        ``application/vnd.amazon.eventstream``.
 
 
 ~~~~~~~~~
@@ -303,7 +304,7 @@ headers for responses:
         is ``application/vnd.amazon.eventstream``.
     * - ``Content-Length``
       - Conditional
-      - The standard ``Content-Length`` header defined by :rfc:`7230#section-3.3.2`.
+      - The standard ``Content-Length`` header defined by :rfc:`9110#section-8.6`.
         For event streaming requests, this SHOULD NOT be set.
 
 
diff --git a/docs/source-2.0/aws/protocols/aws-json.rst.template b/docs/source-2.0/aws/protocols/aws-json.rst.template
index e77914073..0e28b73a1 100644
--- a/docs/source-2.0/aws/protocols/aws-json.rst.template
+++ b/docs/source-2.0/aws/protocols/aws-json.rst.template
@@ -50,7 +50,7 @@ The |quoted shape name| protocol uses the following headers:
     * - ``Content-Length``
       - true
       - The standard ``Content-Length`` header defined by
-        :rfc:`7230#section-3.3.2`.
+        :rfc:`9110#section-8.6`.
     * - ``X-Amz-Target``
       - true for requests
       - The value of this header is the :token:`shape name <smithy:Identifier>` of the
diff --git a/docs/source-2.0/spec/behavior-traits.rst b/docs/source-2.0/spec/behavior-traits.rst
index 7ada59fea..5fdfdb834 100644
--- a/docs/source-2.0/spec/behavior-traits.rst
+++ b/docs/source-2.0/spec/behavior-traits.rst
@@ -6,7 +6,7 @@ Idempotency
 ===========
 
 Operations marked with the :ref:`readonly-trait` or :ref:`idempotent-trait`
-are considered idempotent as defined in :rfc:`7231#section-4.2.2`. Operations
+are considered idempotent as defined in :rfc:`9110#section-9.2.2`. Operations
 that contain a top-level input member marked with the :ref:`idempotencytoken-trait`
 that are provided a token for the member are also considered idempotent. All
 other operations SHOULD be considered unsafe to retry unless the response to
diff --git a/docs/source-2.0/spec/http-bindings.rst b/docs/source-2.0/spec/http-bindings.rst
index fa00f8de0..b177a5454 100644
--- a/docs/source-2.0/spec/http-bindings.rst
+++ b/docs/source-2.0/spec/http-bindings.rst
@@ -14,7 +14,7 @@ and error structures are considered when serializing HTTP messages.
 
 .. important::
 
-    Violating :rfc:`HTTP specifications <7230>` or relying on poorly-supported
+    Violating :rfc:`HTTP specifications <9110>` or relying on poorly-supported
     HTTP functionality when defining HTTP bindings will limit interoperability
     and likely lead to undefined behavior across Smithy implementations. For
     example, avoid defining GET/DELETE requests with payloads, defining
@@ -107,9 +107,8 @@ method
 The ``method`` property defines the HTTP method of the operation (e.g., "GET",
 "PUT", "POST", "DELETE", "PATCH", etc). Smithy will use this value literally
 and will perform no validation on the method. The ``method`` value SHOULD
-match the ``operation`` production rule of :rfc:`7230#appendix-B`. This
-property does not influence the safety or idempotency characteristics of an
-operation.
+match one of the definitions found in :rfc:`9110#section-9.3`. This property
+does not influence the safety or idempotency characteristics of an operation.
 
 
 .. _http-uri:
@@ -118,7 +117,7 @@ uri
 ---
 
 The ``uri`` property defines the *request-target* of the operation in
-*origin-form* as defined in :rfc:`7230#section-5.3.1`. The URI is a simple
+*origin-form* as defined in :rfc:`9112#section-3.2.1`. The URI is a simple
 pattern that Smithy uses to match HTTP requests to operations and to bind
 components of the request URI to fields in the operations's input structure.
 :dfn:`Patterns` consist of literal characters that MUST be matched in the
@@ -634,9 +633,8 @@ Trait selector
     ``structure`` member that targets a list of these types.
 Value type
     ``string`` value defining a valid HTTP header field name according to
-    :rfc:`section 3.2 of RFC7230 <7230#section-3.2>`. The value MUST NOT be
-    empty and MUST be case-insensitively unique across all other members of
-    the structure.
+    :rfc:`9110#section-5.1`. The value MUST NOT be empty and MUST be
+    case-insensitively unique across all other members of the structure.
 Conflicts with
    :ref:`httpLabel-trait`,
    :ref:`httpQuery-trait`,
@@ -656,7 +654,7 @@ Conflicts with
 * ``string`` values with a :ref:`mediaType-trait` are always base64 encoded.
 * ``timestamp`` values are serialized using the ``http-date``
   format by default, as defined in the ``IMF-fixdate`` production of
-  :rfc:`7231#section-7.1.1.1`. The :ref:`timestampFormat-trait` MAY be used
+  :rfc:`9110#section-5.6.7`. The :ref:`timestampFormat-trait` MAY be used
   to use a custom serialization format.
 
 .. note::
@@ -1089,7 +1087,7 @@ is simply ignored.
 .. note::
 
     While there is no limit placed on the length of an
-    :rfc:`HTTP request line <7230#section-3.1.1>`, many HTTP client and server
+    :rfc:`HTTP request line <9112#section-3>`, many HTTP client and server
     implementations enforce limits in practice. Carefully consider the maximum
     allowed length of each member that is bound to an HTTP query string or
     path.
diff --git a/docs/source-2.0/spec/protocol-traits.rst b/docs/source-2.0/spec/protocol-traits.rst
index 9e9341043..18f9518ff 100644
--- a/docs/source-2.0/spec/protocol-traits.rst
+++ b/docs/source-2.0/spec/protocol-traits.rst
@@ -228,7 +228,7 @@ Smithy defines the following built-in timestamp formats:
         values that contain offsets gracefully by normalizing them to UTC.
     * - http-date
       - An HTTP date as defined by the ``IMF-fixdate`` production in
-        :rfc:`7231#section-7.1.1.1` (for example,
+        :rfc:`9110#section-5.6.7` (for example,
         ``Tue, 29 Apr 2014 18:30:38 GMT``). A deserializer that encounters an
         ``http-date`` timestamp with fractional precision SHOULD fail to
         deserialize the value (for example, an HTTP server SHOULD return a 400
diff --git a/docs/source-2.0/spec/service-types.rst b/docs/source-2.0/spec/service-types.rst
index 5ce6ac3d2..c5dd2bcda 100644
--- a/docs/source-2.0/spec/service-types.rst
+++ b/docs/source-2.0/spec/service-types.rst
@@ -862,7 +862,7 @@ Put semantics
 ^^^^^^^^^^^^^
 
 The semantics of a ``put`` lifecycle operation are similar to the semantics
-of a HTTP PUT method as described in :rfc:`section 4.3.4 of [RFC7231] <7231#section-4.3.4>`:
+of a HTTP PUT method as described in :rfc:`section 4.3.4 of [RFC9110] <9110#section-9.3.4>`:
 
   The PUT method requests that the state of the target resource be
   created or replaced ...
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/pattern/UriPattern.java b/smithy-model/src/main/java/software/amazon/smithy/model/pattern/UriPattern.java
index e9847ed2e..7cc90857c 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/pattern/UriPattern.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/pattern/UriPattern.java
@@ -55,12 +55,12 @@ public final class UriPattern extends SmithyPattern {
      * Parse a URI pattern string into a UriPattern.
      *
      * <p>The provided value must match the origin-form request-target
-     * grammar production in RFC 7230, section 5.3.1.
+     * grammar production in RFC 9112, section 3.2.1.
      *
      * @param uri URI pattern to parse.
      * @return Returns the parsed URI pattern.
      * @throws InvalidUriPatternException for invalid URI patterns.
-     * @see <a href="https://tools.ietf.org/html/rfc7230#section-5.3.1">RFC 7230 Section 5.3.1</a>
+     * @see <a href="https://tools.ietf.org/html/rfc9112#section-3.2.1">RFC 9112 Section 3.2.1</a>
      */
     public static UriPattern parse(String uri) {
         if (uri.endsWith("?")) {
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/TimestampFormatPlugin.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/TimestampFormatPlugin.java
index 8518356cd..24919cf17 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/TimestampFormatPlugin.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/TimestampFormatPlugin.java
@@ -126,7 +126,7 @@ final class TimestampFormatPlugin implements NodeValidatorPlugin {
     private String createInvalidHttpDateMessage(String dateValue) {
         return String.format(
                 "Invalid value provided for %s formatted timestamp. Expected a string value that "
-                + "matches the IMF-fixdate production of RFC 7231 section-7.1.1.1. Found: %s",
+                + "matches the IMF-fixdate production of RFC 9110 section-5.6.7. Found: %s",
                 TimestampFormatTrait.HTTP_DATE, dateValue);
     }
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpHeaderTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpHeaderTraitValidator.java
index a438298b1..45dbff2f2 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpHeaderTraitValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpHeaderTraitValidator.java
@@ -39,7 +39,7 @@ import software.amazon.smithy.utils.SetUtils;
  */
 public final class HttpHeaderTraitValidator extends AbstractValidator {
 
-    /** Gather the allowed characters for HTTP headers (tchar from RFC 7230). **/
+    /** Gather the allowed characters for HTTP headers (tchar from RFC 9110 section 5.6.2). **/
     private static final Set<Character> TCHAR = SetUtils.of(
             // "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
             '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~',
@@ -101,7 +101,7 @@ public final class HttpHeaderTraitValidator extends AbstractValidator {
         for (int i = 0; i < header.length(); i++) {
             if (!TCHAR.contains(header.charAt(i))) {
                 return Optional.of(danger(member, trait, String.format(
-                        "`%s` is not a valid HTTP header field name according to section 3.2 of RFC 7230", header)));
+                        "`%s` is not a valid HTTP header field name according to section 5.6.2 of RFC 9110", header)));
             }
         }
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
index 69f38b383..42f660a06 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/HttpMethodSemanticsValidator.java
@@ -35,7 +35,7 @@ import software.amazon.smithy.utils.MapUtils;
 
 /**
  * Validates that `http` traits applied to operation shapes use the most
- * semantically appropriate HTTP method according to RFC 7231.
+ * semantically appropriate HTTP method according to RFC 9110.
  */
 public final class HttpMethodSemanticsValidator extends AbstractValidator {
     /**
diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
index abce5303e..b8a92aff7 100644
--- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
+++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy
@@ -1123,7 +1123,7 @@ enum timestampFormat {
     EPOCH_SECONDS = "epoch-seconds"
 
     /// An HTTP date as defined by the IMF-fixdate production in
-    /// RFC 7231#section-7.1.1.1 (for example, Tue, 29 Apr 2014 18:30:38 GMT).
+    /// RFC 9110#section-5.6.7 (for example, Tue, 29 Apr 2014 18:30:38 GMT).
     HTTP_DATE = "http-date"
 }
 
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/OperationContextParamsChecker.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/OperationContextParamsChecker.java
new file mode 100644
index 000000000..9581d4c6f
--- /dev/null
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/OperationContextParamsChecker.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.rulesengine.analysis;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+import software.amazon.smithy.jmespath.JmespathExpression;
+import software.amazon.smithy.jmespath.LinterResult;
+import software.amazon.smithy.jmespath.RuntimeType;
+import software.amazon.smithy.jmespath.ast.LiteralExpression;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.knowledge.OperationIndex;
+import software.amazon.smithy.model.shapes.BigDecimalShape;
+import software.amazon.smithy.model.shapes.BigIntegerShape;
+import software.amazon.smithy.model.shapes.BlobShape;
+import software.amazon.smithy.model.shapes.BooleanShape;
+import software.amazon.smithy.model.shapes.ByteShape;
+import software.amazon.smithy.model.shapes.DocumentShape;
+import software.amazon.smithy.model.shapes.DoubleShape;
+import software.amazon.smithy.model.shapes.FloatShape;
+import software.amazon.smithy.model.shapes.IntegerShape;
+import software.amazon.smithy.model.shapes.ListShape;
+import software.amazon.smithy.model.shapes.LongShape;
+import software.amazon.smithy.model.shapes.MapShape;
+import software.amazon.smithy.model.shapes.MemberShape;
+import software.amazon.smithy.model.shapes.OperationShape;
+import software.amazon.smithy.model.shapes.ResourceShape;
+import software.amazon.smithy.model.shapes.ServiceShape;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeVisitor;
+import software.amazon.smithy.model.shapes.ShortShape;
+import software.amazon.smithy.model.shapes.StringShape;
+import software.amazon.smithy.model.shapes.StructureShape;
+import software.amazon.smithy.model.shapes.TimestampShape;
+import software.amazon.smithy.model.shapes.UnionShape;
+import software.amazon.smithy.model.traits.LengthTrait;
+import software.amazon.smithy.model.traits.RangeTrait;
+import software.amazon.smithy.rulesengine.language.syntax.parameters.ParameterType;
+import software.amazon.smithy.rulesengine.traits.OperationContextParamDefinition;
+
+public final class OperationContextParamsChecker {
+
+    private OperationContextParamsChecker() {
+
+    }
+
+    public static LinterResult lint(
+            OperationContextParamDefinition paramDefinition, OperationShape operationShape, Model model) {
+        JmespathExpression path = JmespathExpression.parse(paramDefinition.getPath());
+        StructureShape input = OperationIndex.of(model).expectInputShape(operationShape);
+        return path.lint(createCurrentNodeFromShape(input, model));
+    }
+
+    public static Optional<ParameterType> inferParameterType(
+            OperationContextParamDefinition paramDefinition, OperationShape operationShape, Model model) {
+        RuntimeType runtimeType = lint(paramDefinition, operationShape, model).getReturnType();
+        switch (runtimeType) {
+            case BOOLEAN:
+                return Optional.of(ParameterType.BOOLEAN);
+            case STRING:
+                return Optional.of(ParameterType.STRING);
+            case ARRAY:
+                return Optional.of(ParameterType.STRING_ARRAY);
+            default:
+                return Optional.empty();
+        }
+    }
+
+    private static LiteralExpression createCurrentNodeFromShape(Shape shape, Model model) {
+        return shape == null
+                ? LiteralExpression.ANY
+                : new LiteralExpression(shape.accept(new ModelRuntimeTypeGenerator(model)));
+    }
+
+
+    /**
+     * This class is duplicated from
+     * smithy-waiters/src/main/java/software/amazon/smithy/waiters/ModelRuntimeTypeGenerator.java
+     *
+     * It is duplicated here to avoid taking a dependency on smithy-waiters.
+     */
+    private static final class ModelRuntimeTypeGenerator implements ShapeVisitor<Object> {
+
+        private final Model model;
+        private Set<MemberShape> visited = new HashSet<>();
+
+        ModelRuntimeTypeGenerator(Model model) {
+            this.model = model;
+        }
+
+        @Override
+        public Object blobShape(BlobShape shape) {
+            return "blob";
+        }
+
+        @Override
+        public Object booleanShape(BooleanShape shape) {
+            return true;
+        }
+
+        @Override
+        public Object byteShape(ByteShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object shortShape(ShortShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object integerShape(IntegerShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object longShape(LongShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object floatShape(FloatShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object doubleShape(DoubleShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object bigIntegerShape(BigIntegerShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object bigDecimalShape(BigDecimalShape shape) {
+            return computeRange(shape);
+        }
+
+        @Override
+        public Object documentShape(DocumentShape shape) {
+            return LiteralExpression.ANY;
+        }
+
+        @Override
+        public Object stringShape(StringShape shape) {
+            // Create a random string that does not exceed or go under the length trait.
+            int chars = computeLength(shape);
+
+            // Fill a string with "a"'s up to chars.
+            return new String(new char[chars]).replace("\0", "a");
+        }
+
+        @Override
+        public Object listShape(ListShape shape) {
+            return withCopiedVisitors(() -> {
+                int size = computeLength(shape);
+                List<Object> result = new ArrayList<>(size);
+                Object memberValue = shape.getMember().accept(this);
+                if (memberValue != null) {
+                    for (int i = 0; i < size; i++) {
+                        result.add(memberValue);
+                    }
+                }
+                return result;
+            });
+        }
+
+        // Visits members and mutates a copy of the current set of visited
+        // shapes rather than a shared set. This allows a shape to be used
+        // multiple times in the closure of a single shape without causing the
+        // reuse of the shape to always be assumed to be a recursive type.
+        private Object withCopiedVisitors(Supplier<Object> supplier) {
+            // Account for recursive shapes at the current
+            Set<MemberShape> visitedCopy = new HashSet<>(visited);
+            Object result = supplier.get();
+            visited = visitedCopy;
+            return result;
+        }
+
+        @Override
+        public Object mapShape(MapShape shape) {
+            return withCopiedVisitors(() -> {
+                int size = computeLength(shape);
+                Map<String, Object> result = new HashMap<>();
+                String key = (String) shape.getKey().accept(this);
+                Object memberValue = shape.getValue().accept(this);
+                for (int i = 0; i < size; i++) {
+                    result.put(key + i, memberValue);
+                }
+                return result;
+            });
+        }
+
+        @Override
+        public Object structureShape(StructureShape shape) {
+            return structureOrUnion(shape);
+        }
+
+        @Override
+        public Object unionShape(UnionShape shape) {
+            return structureOrUnion(shape);
+        }
+
+        private Object structureOrUnion(Shape shape) {
+            return withCopiedVisitors(() -> {
+                Map<String, Object> result = new LinkedHashMap<>();
+                for (MemberShape member : shape.members()) {
+                    Object memberValue = member.accept(this);
+                    result.put(member.getMemberName(), memberValue);
+                }
+                return result;
+            });
+        }
+
+        @Override
+        public Object memberShape(MemberShape shape) {
+            // Account for recursive shapes.
+            // A false return value means it was in the set.
+            if (!visited.add(shape)) {
+                return LiteralExpression.ANY;
+            }
+
+            return model.getShape(shape.getTarget())
+                    .map(target -> target.accept(this))
+                    // Rather than fail on broken models during waiter validation,
+                    // return an ANY to get *some* validation.
+                    .orElse(LiteralExpression.ANY);
+        }
+
+        @Override
+        public Object timestampShape(TimestampShape shape) {
+            return LiteralExpression.NUMBER;
+        }
+
+        @Override
+        public Object operationShape(OperationShape shape) {
+            throw new UnsupportedOperationException(shape.toString());
+        }
+
+        @Override
+        public Object resourceShape(ResourceShape shape) {
+            throw new UnsupportedOperationException(shape.toString());
+        }
+
+        @Override
+        public Object serviceShape(ServiceShape shape) {
+            throw new UnsupportedOperationException(shape.toString());
+        }
+
+        private int computeLength(Shape shape) {
+            // Create a random string that does not exceed or go under the length trait.
+            int chars = 2;
+
+            if (shape.hasTrait(LengthTrait.class)) {
+                LengthTrait trait = shape.expectTrait(LengthTrait.class);
+                if (trait.getMin().isPresent()) {
+                    chars = Math.max(chars, trait.getMin().get().intValue());
+                }
+                if (trait.getMax().isPresent()) {
+                    chars = Math.min(chars, trait.getMax().get().intValue());
+                }
+            }
+
+            return chars;
+        }
+
+        private double computeRange(Shape shape) {
+            // Create a random string that does not exceed or go under the range trait.
+            double i = 8;
+
+            if (shape.hasTrait(RangeTrait.class)) {
+                RangeTrait trait = shape.expectTrait(RangeTrait.class);
+                if (trait.getMin().isPresent()) {
+                    i = Math.max(i, trait.getMin().get().doubleValue());
+                }
+                if (trait.getMax().isPresent()) {
+                    i = Math.min(i, trait.getMax().get().doubleValue());
+                }
+            }
+
+            return i;
+        }
+    }
+}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTraitValidator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTraitValidator.java
index c652fc1c1..e2666f1c3 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTraitValidator.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/traits/OperationContextParamsTraitValidator.java
@@ -7,13 +7,8 @@ package software.amazon.smithy.rulesengine.traits;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import software.amazon.smithy.jmespath.ExpressionVisitor;
 import software.amazon.smithy.jmespath.JmespathException;
@@ -38,34 +33,10 @@ import software.amazon.smithy.jmespath.ast.ProjectionExpression;
 import software.amazon.smithy.jmespath.ast.SliceExpression;
 import software.amazon.smithy.jmespath.ast.Subexpression;
 import software.amazon.smithy.model.Model;
-import software.amazon.smithy.model.knowledge.OperationIndex;
-import software.amazon.smithy.model.shapes.BigDecimalShape;
-import software.amazon.smithy.model.shapes.BigIntegerShape;
-import software.amazon.smithy.model.shapes.BlobShape;
-import software.amazon.smithy.model.shapes.BooleanShape;
-import software.amazon.smithy.model.shapes.ByteShape;
-import software.amazon.smithy.model.shapes.DocumentShape;
-import software.amazon.smithy.model.shapes.DoubleShape;
-import software.amazon.smithy.model.shapes.FloatShape;
-import software.amazon.smithy.model.shapes.IntegerShape;
-import software.amazon.smithy.model.shapes.ListShape;
-import software.amazon.smithy.model.shapes.LongShape;
-import software.amazon.smithy.model.shapes.MapShape;
-import software.amazon.smithy.model.shapes.MemberShape;
 import software.amazon.smithy.model.shapes.OperationShape;
-import software.amazon.smithy.model.shapes.ResourceShape;
-import software.amazon.smithy.model.shapes.ServiceShape;
-import software.amazon.smithy.model.shapes.Shape;
-import software.amazon.smithy.model.shapes.ShapeVisitor;
-import software.amazon.smithy.model.shapes.ShortShape;
-import software.amazon.smithy.model.shapes.StringShape;
-import software.amazon.smithy.model.shapes.StructureShape;
-import software.amazon.smithy.model.shapes.TimestampShape;
-import software.amazon.smithy.model.shapes.UnionShape;
-import software.amazon.smithy.model.traits.LengthTrait;
-import software.amazon.smithy.model.traits.RangeTrait;
 import software.amazon.smithy.model.validation.AbstractValidator;
 import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.rulesengine.analysis.OperationContextParamsChecker;
 import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.SmithyUnstableApi;
 
@@ -86,8 +57,8 @@ public final class OperationContextParamsTraitValidator extends AbstractValidato
 
                 try {
                     JmespathExpression path = JmespathExpression.parse(entry.getValue().getPath());
-                    StructureShape input = OperationIndex.of(model).expectInputShape(operationShape);
-                    LinterResult linterResult = path.lint(createCurrentNodeFromShape(input, model));
+                    LinterResult linterResult = OperationContextParamsChecker.lint(
+                            entry.getValue(), operationShape, model);
 
                     if (!linterResult.getProblems().isEmpty()) {
                         events.add(error(operationShape,
@@ -134,12 +105,6 @@ public final class OperationContextParamsTraitValidator extends AbstractValidato
         return events;
     }
 
-    private LiteralExpression createCurrentNodeFromShape(Shape shape, Model model) {
-        return shape == null
-                ? LiteralExpression.ANY
-                : new LiteralExpression(shape.accept(new ModelRuntimeTypeGenerator(model)));
-    }
-
     private static final class UnsupportedJmesPathVisitor implements ExpressionVisitor<List<String>> {
 
         @Override
@@ -245,215 +210,4 @@ public final class OperationContextParamsTraitValidator extends AbstractValidato
             return Collections.unmodifiableList(unsupported);
         }
     }
-
-    /**
-     * This class is duplicated from
-     * smithy-waiters/src/main/java/software/amazon/smithy/waiters/ModelRuntimeTypeGenerator.java
-     *
-     * It is duplicated here to avoid taking a dependency on smithy-waiters.
-     */
-    private static final class ModelRuntimeTypeGenerator implements ShapeVisitor<Object> {
-
-        private final Model model;
-        private Set<MemberShape> visited = new HashSet<>();
-
-        ModelRuntimeTypeGenerator(Model model) {
-            this.model = model;
-        }
-
-        @Override
-        public Object blobShape(BlobShape shape) {
-            return "blob";
-        }
-
-        @Override
-        public Object booleanShape(BooleanShape shape) {
-            return true;
-        }
-
-        @Override
-        public Object byteShape(ByteShape shape) {
-            return computeRange(shape);
-        }
-
-        @Override
-        public Object shortShape(ShortShape shape) {
-            return computeRange(shape);
-        }
-
-        @Override
-        public Object integerShape(IntegerShape shape) {
-            return computeRange(shape);
-        }
-
-        @Override
-        public Object longShape(LongShape shape) {
-            return computeRange(shape);
-        }
-
-        @Override
-        public Object floatShape(FloatShape shape) {
-            return computeRange(shape);
-        }
-
-        @Override
-        public Object doubleShape(DoubleShape shape) {
-            return computeRange(shape);
-        }
-
-        @Override
-        public Object bigIntegerShape(BigIntegerShape shape) {
-            return computeRange(shape);
-        }
-
-        @Override
-        public Object bigDecimalShape(BigDecimalShape shape) {
-            return computeRange(shape);
-        }
-
-        @Override
-        public Object documentShape(DocumentShape shape) {
-            return LiteralExpression.ANY;
-        }
-
-        @Override
-        public Object stringShape(StringShape shape) {
-            // Create a random string that does not exceed or go under the length trait.
-            int chars = computeLength(shape);
-
-            // Fill a string with "a"'s up to chars.
-            return new String(new char[chars]).replace("\0", "a");
-        }
-
-        @Override
-        public Object listShape(ListShape shape) {
-            return withCopiedVisitors(() -> {
-                int size = computeLength(shape);
-                List<Object> result = new ArrayList<>(size);
-                Object memberValue = shape.getMember().accept(this);
-                if (memberValue != null) {
-                    for (int i = 0; i < size; i++) {
-                        result.add(memberValue);
-                    }
-                }
-                return result;
-            });
-        }
-
-        // Visits members and mutates a copy of the current set of visited
-        // shapes rather than a shared set. This allows a shape to be used
-        // multiple times in the closure of a single shape without causing the
-        // reuse of the shape to always be assumed to be a recursive type.
-        private Object withCopiedVisitors(Supplier<Object> supplier) {
-            // Account for recursive shapes at the current
-            Set<MemberShape> visitedCopy = new HashSet<>(visited);
-            Object result = supplier.get();
-            visited = visitedCopy;
-            return result;
-        }
-
-        @Override
-        public Object mapShape(MapShape shape) {
-            return withCopiedVisitors(() -> {
-                int size = computeLength(shape);
-                Map<String, Object> result = new HashMap<>();
-                String key = (String) shape.getKey().accept(this);
-                Object memberValue = shape.getValue().accept(this);
-                for (int i = 0; i < size; i++) {
-                    result.put(key + i, memberValue);
-                }
-                return result;
-            });
-        }
-
-        @Override
-        public Object structureShape(StructureShape shape) {
-            return structureOrUnion(shape);
-        }
-
-        @Override
-        public Object unionShape(UnionShape shape) {
-            return structureOrUnion(shape);
-        }
-
-        private Object structureOrUnion(Shape shape) {
-            return withCopiedVisitors(() -> {
-                Map<String, Object> result = new LinkedHashMap<>();
-                for (MemberShape member : shape.members()) {
-                    Object memberValue = member.accept(this);
-                    result.put(member.getMemberName(), memberValue);
-                }
-                return result;
-            });
-        }
-
-        @Override
-        public Object memberShape(MemberShape shape) {
-            // Account for recursive shapes.
-            // A false return value means it was in the set.
-            if (!visited.add(shape)) {
-                return LiteralExpression.ANY;
-            }
-
-            return model.getShape(shape.getTarget())
-                    .map(target -> target.accept(this))
-                    // Rather than fail on broken models during waiter validation,
-                    // return an ANY to get *some* validation.
-                    .orElse(LiteralExpression.ANY);
-        }
-
-        @Override
-        public Object timestampShape(TimestampShape shape) {
-            return LiteralExpression.NUMBER;
-        }
-
-        @Override
-        public Object operationShape(OperationShape shape) {
-            throw new UnsupportedOperationException(shape.toString());
-        }
-
-        @Override
-        public Object resourceShape(ResourceShape shape) {
-            throw new UnsupportedOperationException(shape.toString());
-        }
-
-        @Override
-        public Object serviceShape(ServiceShape shape) {
-            throw new UnsupportedOperationException(shape.toString());
-        }
-
-        private int computeLength(Shape shape) {
-            // Create a random string that does not exceed or go under the length trait.
-            int chars = 2;
-
-            if (shape.hasTrait(LengthTrait.class)) {
-                LengthTrait trait = shape.expectTrait(LengthTrait.class);
-                if (trait.getMin().isPresent()) {
-                    chars = Math.max(chars, trait.getMin().get().intValue());
-                }
-                if (trait.getMax().isPresent()) {
-                    chars = Math.min(chars, trait.getMax().get().intValue());
-                }
-            }
-
-            return chars;
-        }
-
-        private double computeRange(Shape shape) {
-            // Create a random string that does not exceed or go under the range trait.
-            double i = 8;
-
-            if (shape.hasTrait(RangeTrait.class)) {
-                RangeTrait trait = shape.expectTrait(RangeTrait.class);
-                if (trait.getMin().isPresent()) {
-                    i = Math.max(i, trait.getMin().get().doubleValue());
-                }
-                if (trait.getMax().isPresent()) {
-                    i = Math.min(i, trait.getMax().get().doubleValue());
-                }
-            }
-
-            return i;
-        }
-    }
 }
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/RuleSetParameterValidator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/RuleSetParameterValidator.java
index a08d3bc0b..27f6d9f45 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/RuleSetParameterValidator.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/RuleSetParameterValidator.java
@@ -10,6 +10,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import software.amazon.smithy.model.FromSourceLocation;
 import software.amazon.smithy.model.Model;
@@ -22,6 +23,7 @@ import software.amazon.smithy.model.shapes.Shape;
 import software.amazon.smithy.model.shapes.StructureShape;
 import software.amazon.smithy.model.validation.AbstractValidator;
 import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.rulesengine.analysis.OperationContextParamsChecker;
 import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
 import software.amazon.smithy.rulesengine.language.evaluation.value.Value;
 import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter;
@@ -34,6 +36,7 @@ import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;
 import software.amazon.smithy.rulesengine.traits.EndpointTestCase;
 import software.amazon.smithy.rulesengine.traits.EndpointTestOperationInput;
 import software.amazon.smithy.rulesengine.traits.EndpointTestsTrait;
+import software.amazon.smithy.rulesengine.traits.OperationContextParamsTrait;
 import software.amazon.smithy.rulesengine.traits.StaticContextParamDefinition;
 import software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait;
 import software.amazon.smithy.utils.ListUtils;
@@ -80,7 +83,7 @@ public final class RuleSetParameterValidator extends AbstractValidator {
 
         if (serviceShape.hasTrait(ClientContextParamsTrait.class)) {
             ClientContextParamsTrait trait = serviceShape.expectTrait(ClientContextParamsTrait.class);
-            for (Map.Entry<String, ClientContextParamDefinition> entry : trait.getParameters().entrySet())  {
+            for (Map.Entry<String, ClientContextParamDefinition> entry : trait.getParameters().entrySet()) {
                 endpointParams.put(entry.getKey(), Parameter.builder()
                         .name(entry.getKey())
                         .type(ParameterType.fromShapeType(entry.getValue().getType()))
@@ -108,6 +111,26 @@ public final class RuleSetParameterValidator extends AbstractValidator {
                 }
             }
 
+            if (operationShape.hasTrait(OperationContextParamsTrait.ID)) {
+                OperationContextParamsTrait trait = operationShape.expectTrait(OperationContextParamsTrait.class);
+                trait.getParameters().forEach((name, p) -> {
+                    Optional<ParameterType> maybeType = OperationContextParamsChecker
+                            .inferParameterType(p, operationShape, model);
+                    maybeType.ifPresent(parameterType -> {
+                        if (endpointParams.containsKey(name) && endpointParams.get(name).getType() != parameterType) {
+                            errors.add(parameterError(operationShape, trait,
+                                    "OperationContextParams.InconsistentType",
+                                    String.format("Inconsistent type for `%s` parameter", name)));
+                        } else {
+                            endpointParams.put(name, Parameter.builder()
+                                    .name(name)
+                                    .type(parameterType)
+                                    .build());
+                        }
+                    });
+                });
+            }
+
             StructureShape input = model.expectShape(operationShape.getInputShape(), StructureShape.class);
             for (MemberShape memberShape : input.members()) {
                 if (memberShape.hasTrait(ContextParamTrait.class)) {
@@ -200,7 +223,7 @@ public final class RuleSetParameterValidator extends AbstractValidator {
             // All test parameter types from corresponding ruleset parameters must match in all test cases.
             if (!testSuiteHasParam) {
                 errors.add(danger(serviceShape, parameter,
-                                String.format("Parameter `%s` is never used in an `EndpointTests` test case", name))
+                        String.format("Parameter `%s` is never used in an `EndpointTests` test case", name))
                         .toBuilder()
                         .id(getName() + ".TestCase.Unused").build());
             } else {
@@ -208,7 +231,7 @@ public final class RuleSetParameterValidator extends AbstractValidator {
                     if (testParam.getType() != parameter.getType()) {
                         errors.add(parameterError(serviceShape, testParam, "TestCase.TypeMismatch",
                                 String.format("Type mismatch for parameter `%s`, `%s` expected",
-                                testParam.getName().toString(), parameter.getType())));
+                                        testParam.getName().toString(), parameter.getType())));
                     }
                 }
             }
@@ -302,11 +325,11 @@ public final class RuleSetParameterValidator extends AbstractValidator {
     private Parameter buildParameter(String name, Node node) {
         Value value = Value.fromNode(node);
         return Parameter.builder()
-                       .sourceLocation(value)
-                       .name(name)
-                       .value(value)
-                       .type(ParameterType.fromType(value.getType()))
-                       .build();
+                .sourceLocation(value)
+                .name(name)
+                .value(value)
+                .type(ParameterType.fromType(value.getType()))
+                .build();
     }
 
     private List<Parameter> merge(List<Parameter> previousList, List<Parameter> newList) {
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/MediaType.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/MediaType.java
index 96492e025..66ff16794 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/MediaType.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/MediaType.java
@@ -26,7 +26,7 @@ import java.util.Set;
 
 /**
  * Implements a simple media type parser based on the Content-Type grammar defined in
- * <a href="https://tools.ietf.org/html/rfc7231#section-3.1.1.1">RFC 7231</a>.
+ * <a href="https://tools.ietf.org/html/rfc9110#section-8.3">RFC 9110</a>.
  *
  * <p>The type, subtype, and parameter names are all canonicalized to
  * lowercase strings.
@@ -154,7 +154,7 @@ public final class MediaType {
     }
 
     private static final class Parser extends SimpleParser {
-        // See https://tools.ietf.org/html/rfc7230#section-3.2.6
+        // See https://tools.ietf.org/html/rfc9110#section-5.6.2
         // token          = 1*tchar
         // tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
         //                / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
@@ -198,7 +198,7 @@ public final class MediaType {
         }
 
         private void parse() {
-            // From: https://tools.ietf.org/html/rfc7231#section-3.1.1.1
+            // From: https://tools.ietf.org/html/rfc9110#section-8.3.1
             // The type, subtype, and parameter name tokens are case-insensitive.
             //     media-type = type "/" subtype *( OWS ";" OWS parameter )
             //     type       = token.
@@ -240,7 +240,7 @@ public final class MediaType {
             return sliceFrom(start);
         }
 
-        // See https://tools.ietf.org/html/rfc7230#section-3.2.6
+        // See https://tools.ietf.org/html/rfc9110#section-5.6.4
         // quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
         // qdtext         = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
         // obs-text       = %x80-FF


Test patch:
diff --git a/smithy-aws-protocol-tests/model/awsJson1_0/content-type.smithy b/smithy-aws-protocol-tests/model/awsJson1_0/content-type.smithy
new file mode 100644
index 000000000..db83af8a0
--- /dev/null
+++ b/smithy-aws-protocol-tests/model/awsJson1_0/content-type.smithy
@@ -0,0 +1,44 @@
+// This file defines test cases that test Content-Type headers.
+
+$version: "2.0"
+
+namespace aws.protocoltests.json10
+
+use aws.protocols#awsJson1_0
+use smithy.test#httpRequestTests
+use smithy.test#httpResponseTests
+
+/// The example tests how servers must support requests
+/// containing a `Content-Type` header with parameters.
+operation ContentTypeParameters {
+    input: ContentTypeParametersInput,
+    output: ContentTypeParametersOutput
+}
+
+apply ContentTypeParameters @httpRequestTests([
+    {
+        id: "AwsJson10MustSupportParametersInContentType",
+        documentation: "A server should ignore parameters added to the content type",
+        protocol: awsJson1_0,
+        method: "POST",
+        headers: {
+            "Content-Type": "application/x-amz-json-1.0; charset=utf-8",
+            "X-Amz-Target": "JsonRpc10.ContentTypeParameters",
+        },
+        uri: "/",
+        body: "{\"value\":5}",
+        bodyMediaType: "application/json",
+        params: {
+            value: 5,
+        },
+        appliesTo: "server"
+    }
+])
+
+@input
+structure ContentTypeParametersInput {
+    value: Integer,
+}
+
+@output
+structure ContentTypeParametersOutput {}
diff --git a/smithy-aws-protocol-tests/model/awsJson1_0/main.smithy b/smithy-aws-protocol-tests/model/awsJson1_0/main.smithy
index 091a5c93b..6204e414e 100644
--- a/smithy-aws-protocol-tests/model/awsJson1_0/main.smithy
+++ b/smithy-aws-protocol-tests/model/awsJson1_0/main.smithy
@@ -35,6 +35,9 @@ service JsonRpc10 {
         // requestCompression trait tests
         PutWithContentEncoding,
 
+        // Content-Type header tests
+        ContentTypeParameters,
+
         OperationWithDefaults,
         OperationWithRequiredMembers,
         OperationWithNestedStructure
diff --git a/smithy-aws-protocol-tests/model/awsJson1_1/content-type.smithy b/smithy-aws-protocol-tests/model/awsJson1_1/content-type.smithy
new file mode 100644
index 000000000..4b1159e0c
--- /dev/null
+++ b/smithy-aws-protocol-tests/model/awsJson1_1/content-type.smithy
@@ -0,0 +1,44 @@
+// This file defines test cases that test Content-Type headers.
+
+$version: "2.0"
+
+namespace aws.protocoltests.json
+
+use aws.protocols#awsJson1_1
+use smithy.test#httpRequestTests
+use smithy.test#httpResponseTests
+
+/// The example tests how servers must support requests
+/// containing a `Content-Type` header with parameters.
+operation ContentTypeParameters {
+    input: ContentTypeParametersInput,
+    output: ContentTypeParametersOutput
+}
+
+apply ContentTypeParameters @httpRequestTests([
+    {
+        id: "AwsJson11MustSupportParametersInContentType",
+        documentation: "A server should ignore parameters added to the content type",
+        protocol: awsJson1_1,
+        method: "POST",
+        headers: {
+            "Content-Type": "application/x-amz-json-1.1; charset=utf-8",
+            "X-Amz-Target": "JsonProtocol.ContentTypeParameters",
+        },
+        uri: "/",
+        body: "{\"value\":5}",
+        bodyMediaType: "application/json",
+        params: {
+            value: 5,
+        },
+        appliesTo: "server"
+    }
+])
+
+@input
+structure ContentTypeParametersInput {
+    value: Integer,
+}
+
+@output
+structure ContentTypeParametersOutput {}
diff --git a/smithy-aws-protocol-tests/model/awsJson1_1/main.smithy b/smithy-aws-protocol-tests/model/awsJson1_1/main.smithy
index 9503e7df7..fcd1c29c9 100644
--- a/smithy-aws-protocol-tests/model/awsJson1_1/main.smithy
+++ b/smithy-aws-protocol-tests/model/awsJson1_1/main.smithy
@@ -44,6 +44,9 @@ service JsonProtocol {
 
         // requestCompression trait tests
         PutWithContentEncoding
+
+        // Content-Type header tests
+        ContentTypeParameters,
     ],
 }
 
diff --git a/smithy-aws-protocol-tests/model/restJson1/content-type.smithy b/smithy-aws-protocol-tests/model/restJson1/content-type.smithy
new file mode 100644
index 000000000..f003441f0
--- /dev/null
+++ b/smithy-aws-protocol-tests/model/restJson1/content-type.smithy
@@ -0,0 +1,41 @@
+// This file defines test cases that test Content-Type headers.
+
+$version: "2.0"
+
+namespace aws.protocoltests.restjson
+
+use aws.protocols#restJson1
+use aws.protocoltests.shared#DateTime
+use smithy.test#httpRequestTests
+
+/// The example tests how servers must support requests
+/// containing a `Content-Type` header with parameters.
+@http(uri: "/ContentTypeParameters", method: "POST")
+operation ContentTypeParameters {
+    input: ContentTypeParametersInput,
+    output: ContentTypeParametersOutput,
+}
+
+apply ContentTypeParameters @httpRequestTests([
+    {
+        id: "RestJsonMustSupportParametersInContentType",
+        documentation: "A server should ignore parameters added to the content type",
+        uri: "/ContentTypeParameters",
+        method: "POST",
+        protocol: "aws.protocols#restJson1",
+        body: "{\"value\":5}",
+        headers: { "Content-Type": "application/json; charset=utf-8" },
+        params: {
+            value: 5,
+        },
+        appliesTo: "server",
+    },
+])
+
+@input
+structure ContentTypeParametersInput {
+    value: Integer,
+}
+
+@output
+structure ContentTypeParametersOutput {}
diff --git a/smithy-aws-protocol-tests/model/restJson1/defaults.smithy b/smithy-aws-protocol-tests/model/restJson1/defaults.smithy
new file mode 100644
index 000000000..f14363293
--- /dev/null
+++ b/smithy-aws-protocol-tests/model/restJson1/defaults.smithy
@@ -0,0 +1,460 @@
+$version: "2.0"
+
+namespace aws.protocoltests.restjson
+
+use aws.protocols#restJson1
+use smithy.test#httpRequestTests
+use smithy.test#httpResponseTests
+
+apply OperationWithDefaults @httpRequestTests([
+    {
+        id: "RestJsonClientPopulatesDefaultValuesInInput"
+        documentation: "Client populates default values in input."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/OperationWithDefaults"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "defaults": {
+                    "defaultString": "hi",
+                    "defaultBoolean": true,
+                    "defaultList": [],
+                    "defaultDocumentMap": {},
+                    "defaultDocumentString": "hi",
+                    "defaultDocumentBoolean": true,
+                    "defaultDocumentList": [],
+                    "defaultTimestamp": 0,
+                    "defaultBlob": "YWJj",
+                    "defaultByte": 1,
+                    "defaultShort": 1,
+                    "defaultInteger": 10,
+                    "defaultLong": 100,
+                    "defaultFloat": 1.0,
+                    "defaultDouble": 1.0,
+                    "defaultMap": {},
+                    "defaultEnum": "FOO",
+                    "defaultIntEnum": 1,
+                    "emptyString": "",
+                    "falseBoolean": false,
+                    "emptyBlob": "",
+                    "zeroByte": 0,
+                    "zeroShort": 0,
+                    "zeroInteger": 0,
+                    "zeroLong": 0,
+                    "zeroFloat": 0.0,
+                    "zeroDouble": 0.0
+                }
+            }"""
+        params: {
+            defaults: {}
+        }
+    }
+    {
+        id: "RestJsonClientSkipsTopLevelDefaultValuesInInput"
+        documentation: "Client skips top level default values in input."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/OperationWithDefaults"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+            }"""
+        params: {
+        }
+    }
+    {
+        id: "RestJsonClientUsesExplicitlyProvidedMemberValuesOverDefaults"
+        documentation: "Client uses explicitly provided member values over defaults"
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/OperationWithDefaults"
+        headers: {"Content-Type": "application/json"}
+        params: {
+            defaults: {
+                defaultString: "bye"
+                defaultBoolean: true
+                defaultList: ["a"]
+                defaultDocumentMap: {name: "Jack"}
+                defaultDocumentString: "bye"
+                defaultDocumentBoolean: true
+                defaultDocumentList: ["b"]
+                defaultNullDocument: "notNull"
+                defaultTimestamp: 1
+                defaultBlob: "hi"
+                defaultByte: 2
+                defaultShort: 2
+                defaultInteger: 20
+                defaultLong: 200
+                defaultFloat: 2.0
+                defaultDouble: 2.0
+                defaultMap: {name: "Jack"}
+                defaultEnum: "BAR"
+                defaultIntEnum: 2
+                emptyString: "foo"
+                falseBoolean: true
+                emptyBlob: "hi"
+                zeroByte: 1
+                zeroShort: 1
+                zeroInteger: 1
+                zeroLong: 1
+                zeroFloat: 1.0
+                zeroDouble: 1.0
+            }
+        }
+        body: """
+            {
+                "defaults": {
+                    "defaultString": "bye",
+                    "defaultBoolean": true,
+                    "defaultList": ["a"],
+                    "defaultDocumentMap": {"name": "Jack"},
+                    "defaultDocumentString": "bye",
+                    "defaultDocumentBoolean": true,
+                    "defaultDocumentList": ["b"],
+                    "defaultNullDocument": "notNull",
+                    "defaultTimestamp": 1,
+                    "defaultBlob": "aGk=",
+                    "defaultByte": 2,
+                    "defaultShort": 2,
+                    "defaultInteger": 20,
+                    "defaultLong": 200,
+                    "defaultFloat": 2.0,
+                    "defaultDouble": 2.0,
+                    "defaultMap": {"name": "Jack"},
+                    "defaultEnum": "BAR",
+                    "defaultIntEnum": 2,
+                    "emptyString": "foo",
+                    "falseBoolean": true,
+                    "emptyBlob": "aGk=",
+                    "zeroByte": 1,
+                    "zeroShort": 1,
+                    "zeroInteger": 1,
+                    "zeroLong": 1,
+                    "zeroFloat": 1.0,
+                    "zeroDouble": 1.0
+                }
+            }"""
+    }
+    {
+        id: "RestJsonServerPopulatesDefaultsWhenMissingInRequestBody"
+        documentation: "Server populates default values when missing in request body."
+        appliesTo: "server"
+        tags: ["defaults"]
+        protocol: restJson1
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/OperationWithDefaults"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+            "defaults": {}
+            }"""
+        params: {
+            defaults: {
+                defaultString: "hi"
+                defaultBoolean: true
+                defaultList: []
+                defaultDocumentMap: {}
+                defaultDocumentString: "hi"
+                defaultDocumentBoolean: true
+                defaultDocumentList: []
+                defaultTimestamp: 0
+                defaultBlob: "abc"
+                defaultByte: 1
+                defaultShort: 1
+                defaultInteger: 10
+                defaultLong: 100
+                defaultFloat: 1.0
+                defaultDouble: 1.0
+                defaultMap: {}
+                defaultEnum: "FOO"
+                defaultIntEnum: 1
+                emptyString: ""
+                falseBoolean: false
+                emptyBlob: ""
+                zeroByte: 0
+                zeroShort: 0
+                zeroInteger: 0
+                zeroLong: 0
+                zeroFloat: 0.0
+                zeroDouble: 0.0
+            },
+            topLevelDefault: "hi"
+            otherTopLevelDefault: 0
+        }
+    }
+    {
+        id: "RestJsonClientUsesExplicitlyProvidedValuesInTopLevel"
+        documentation: "Any time a value is provided for a member in the top level of input, it is used, regardless of if its the default."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/OperationWithDefaults"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "topLevelDefault": "hi",
+                "otherTopLevelDefault": 0
+            }"""
+        params: {
+            topLevelDefault: "hi"
+            otherTopLevelDefault: 0
+        }
+    }
+    {
+        id: "RestJsonClientIgnoresNonTopLevelDefaultsOnMembersWithClientOptional"
+        documentation: "Typically, non top-level members would have defaults filled in, but if they have the clientOptional trait, the defaults should be ignored."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/OperationWithDefaults"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "clientOptionalDefaults": {}
+            }"""
+        params: {
+            clientOptionalDefaults: {}
+        }
+    }
+])
+
+apply OperationWithDefaults @httpResponseTests([
+    {
+        id: "RestJsonClientPopulatesDefaultsValuesWhenMissingInResponse"
+        documentation: "Client populates default values when missing in response."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        code: 200
+        bodyMediaType: "application/json"
+        headers: {"Content-Type": "application/json"}
+        body: "{}"
+        params: {
+            defaultString: "hi"
+            defaultBoolean: true
+            defaultList: []
+            defaultDocumentMap: {}
+            defaultDocumentString: "hi"
+            defaultDocumentBoolean: true
+            defaultDocumentList: []
+            defaultTimestamp: 0
+            defaultBlob: "abc"
+            defaultByte: 1
+            defaultShort: 1
+            defaultInteger: 10
+            defaultLong: 100
+            defaultFloat: 1.0
+            defaultDouble: 1.0
+            defaultMap: {}
+            defaultEnum: "FOO"
+            defaultIntEnum: 1
+            emptyString: ""
+            falseBoolean: false
+            emptyBlob: ""
+            zeroByte: 0
+            zeroShort: 0
+            zeroInteger: 0
+            zeroLong: 0
+            zeroFloat: 0.0
+            zeroDouble: 0.0
+        }
+    }
+    {
+        id: "RestJsonClientIgnoresDefaultValuesIfMemberValuesArePresentInResponse"
+        documentation: "Client ignores default values if member values are present in the response."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        code: 200
+        bodyMediaType: "application/json"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "defaultString": "bye",
+                "defaultBoolean": false,
+                "defaultList": ["a"],
+                "defaultDocumentMap": {"name": "Jack"},
+                "defaultDocumentString": "bye",
+                "defaultDocumentBoolean": false,
+                "defaultDocumentList": ["b"],
+                "defaultNullDocument": "notNull",
+                "defaultTimestamp": 2,
+                "defaultBlob": "aGk=",
+                "defaultByte": 2,
+                "defaultShort": 2,
+                "defaultInteger": 20,
+                "defaultLong": 200,
+                "defaultFloat": 2.0,
+                "defaultDouble": 2.0,
+                "defaultMap": {"name": "Jack"},
+                "defaultEnum": "BAR",
+                "defaultIntEnum": 2,
+                "emptyString": "foo",
+                "falseBoolean": true,
+                "emptyBlob": "aGk=",
+                "zeroByte": 1,
+                "zeroShort": 1,
+                "zeroInteger": 1,
+                "zeroLong": 1,
+                "zeroFloat": 1.0,
+                "zeroDouble": 1.0
+            }"""
+        params: {
+            defaultString: "bye"
+            defaultBoolean: false
+            defaultList: ["a"]
+            defaultDocumentMap: {name: "Jack"}
+            defaultDocumentString: "bye"
+            defaultDocumentBoolean: false
+            defaultDocumentList: ["b"]
+            defaultNullDocument: "notNull"
+            defaultTimestamp: 1
+            defaultBlob: "hi"
+            defaultByte: 2
+            defaultShort: 2
+            defaultInteger: 20
+            defaultLong: 200
+            defaultFloat: 2.0
+            defaultDouble: 2.0
+            defaultMap: {name: "Jack"}
+            defaultEnum: "BAR"
+            defaultIntEnum: 2
+            emptyString: "foo"
+            falseBoolean: true
+            emptyBlob: "hi"
+            zeroByte: 1
+            zeroShort: 1
+            zeroInteger: 1
+            zeroLong: 1
+            zeroFloat: 1.0
+            zeroDouble: 1.0
+        }
+    }
+    {
+        id: "RestJsonServerPopulatesDefaultsInResponseWhenMissingInParams"
+        documentation: "Server populates default values in response when missing in params."
+        appliesTo: "server"
+        tags: ["defaults"]
+        protocol: restJson1
+        code: 200
+        bodyMediaType: "application/json"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "defaultString": "hi",
+                "defaultBoolean": true,
+                "defaultList": [],
+                "defaultDocumentMap": {},
+                "defaultDocumentString": "hi",
+                "defaultDocumentBoolean": true,
+                "defaultDocumentList": [],
+                "defaultTimestamp": 0,
+                "defaultBlob": "YWJj",
+                "defaultByte": 1,
+                "defaultShort": 1,
+                "defaultInteger": 10,
+                "defaultLong": 100,
+                "defaultFloat": 1.0,
+                "defaultDouble": 1.0,
+                "defaultMap": {},
+                "defaultEnum": "FOO",
+                "defaultIntEnum": 1,
+                "emptyString": "",
+                "falseBoolean": false,
+                "emptyBlob": "",
+                "zeroByte": 0,
+                "zeroShort": 0,
+                "zeroInteger": 0,
+                "zeroLong": 0,
+                "zeroFloat": 0.0,
+                "zeroDouble": 0.0
+            }"""
+        params: {}
+    }
+])
+
+@http(uri: "/OperationWithDefaults", method: "POST")
+operation OperationWithDefaults {
+    input := {
+        defaults: Defaults
+        clientOptionalDefaults: ClientOptionalDefaults
+        topLevelDefault: String = "hi" // Client should ignore default values in input shape
+        otherTopLevelDefault: Integer = 0
+    }
+
+    output := with [DefaultsMixin] {}
+}
+
+structure Defaults with [DefaultsMixin] {}
+
+structure ClientOptionalDefaults {
+    @clientOptional
+    member: Integer = 0
+}
+
+@mixin
+structure DefaultsMixin {
+    defaultString: String = "hi"
+    defaultBoolean: Boolean = true
+    defaultList: TestStringList = []
+    defaultDocumentMap: Document = {}
+    defaultDocumentString: Document = "hi"
+    defaultDocumentBoolean: Document = true
+    defaultDocumentList: Document = []
+    defaultNullDocument: Document = null
+    defaultTimestamp: Timestamp = 0
+    defaultBlob: Blob = "abc"
+    defaultByte: Byte = 1
+    defaultShort: Short = 1
+    defaultInteger: Integer = 10
+    defaultLong: Long = 100
+    defaultFloat: Float = 1.0
+    defaultDouble: Double = 1.0
+    defaultMap: TestStringMap = {}
+    defaultEnum: TestEnum = "FOO"
+    defaultIntEnum: TestIntEnum = 1
+    emptyString: String = ""
+    falseBoolean: Boolean = false
+    emptyBlob: Blob = ""
+    zeroByte: Byte = 0
+    zeroShort: Short = 0
+    zeroInteger: Integer = 0
+    zeroLong: Long = 0
+    zeroFloat: Float = 0.0
+    zeroDouble: Double = 0.0
+}
+
+list TestStringList {
+    member: String
+}
+
+map TestStringMap {
+    key: String
+    value: String
+}
+
+enum TestEnum {
+    FOO
+    BAR
+    BAZ
+}
+
+intEnum TestIntEnum {
+    ONE = 1
+    TWO = 2
+}
\ No newline at end of file
diff --git a/smithy-aws-protocol-tests/model/restJson1/main.smithy b/smithy-aws-protocol-tests/model/restJson1/main.smithy
index e5ae6f979..2a414502b 100644
--- a/smithy-aws-protocol-tests/model/restJson1/main.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/main.smithy
@@ -151,6 +151,13 @@ service RestJson {
         FractionalSeconds,
 
         // requestCompression trait tests
-        PutWithContentEncoding
+        PutWithContentEncoding,
+
+        // Content-Type header tests
+        ContentTypeParameters,
+
+        // defaults
+        OperationWithDefaults
+        OperationWithNestedStructure
     ]
 }
diff --git a/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-union.smithy b/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-union.smithy
index cd0ca8cd4..ab4571cdb 100644
--- a/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-union.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/malformedRequests/malformed-union.smithy
@@ -78,6 +78,28 @@ apply MalformedUnion @httpMalformedRequestTests([
             }
         }
     },
+    {
+        id: "RestJsonMalformedUnionEmptyObjectNoFieldsSet",
+        documentation: """
+            When the union is an empty object, it has no fields set, so the
+            response should be a 400 SerializationException.""",
+        protocol: restJson1,
+        request: {
+            method: "POST",
+            uri: "/MalformedUnion",
+            body: """
+                { "union" : {  } }""",
+            headers: {
+                "content-type": "application/json"
+            }
+        },
+        response: {
+            code: 400,
+            headers: {
+                "x-amzn-errortype": "SerializationException"
+            }
+        }
+    },
     {
         id: "RestJsonMalformedUnionValueIsArray",
         documentation: """
diff --git a/smithy-aws-protocol-tests/model/restJson1/nested-defaults.smithy b/smithy-aws-protocol-tests/model/restJson1/nested-defaults.smithy
new file mode 100644
index 000000000..bba92f85e
--- /dev/null
+++ b/smithy-aws-protocol-tests/model/restJson1/nested-defaults.smithy
@@ -0,0 +1,409 @@
+$version: "2.0"
+
+namespace aws.protocoltests.restjson
+
+use aws.protocols#restJson1
+use smithy.test#httpRequestTests
+use smithy.test#httpResponseTests
+
+apply OperationWithNestedStructure @httpRequestTests([
+    {
+        id: "RestJsonClientPopulatesNestedDefaultValuesWhenMissing"
+        documentation: "Client populates nested default values when missing."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/OperationWithNestedStructure"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "topLevel": {
+                    "dialog": {
+                        "language": "en",
+                        "greeting": "hi"
+                    },
+                    "dialogList": [
+                        {
+                            "greeting": "hi"
+                        },
+                        {
+                            "greeting": "hi",
+                            "farewell": {
+                                "phrase": "bye"
+                            }
+                        },
+                        {
+                            "language": "it",
+                            "greeting": "ciao",
+                            "farewell": {
+                                "phrase": "arrivederci"
+                            }
+                        }
+                    ],
+                    "dialogMap": {
+                        "emptyDialog": {
+                            "greeting": "hi"
+                        },
+                        "partialEmptyDialog": {
+                            "language": "en",
+                            "greeting": "hi",
+                            "farewell": {
+                                "phrase": "bye"
+                            }
+                        },
+                        "nonEmptyDialog": {
+                            "greeting": "konnichiwa",
+                            "farewell": {
+                                "phrase": "sayonara"
+                            }
+                        }
+                    }
+                }
+            }"""
+        params: {
+            "topLevel": {
+                "dialog": {
+                    "language": "en"
+                },
+                "dialogList": [
+                    {
+                    },
+                    {
+                        "farewell": {}
+                    },
+                    {
+                        "language": "it",
+                        "greeting": "ciao",
+                        "farewell": {
+                            "phrase": "arrivederci"
+                        }
+                    }
+                ],
+                "dialogMap": {
+                    "emptyDialog": {
+                    },
+                    "partialEmptyDialog": {
+                        "language": "en",
+                        "farewell": {}
+                    },
+                    "nonEmptyDialog": {
+                        "greeting": "konnichiwa",
+                        "farewell": {
+                            "phrase": "sayonara"
+                        }
+                    }
+                }
+            }
+        }
+    }
+    {
+        id: "RestJsonServerPopulatesNestedDefaultsWhenMissingInRequestBody"
+        documentation: "Server populates nested default values when missing in request body."
+        appliesTo: "server"
+        tags: ["defaults"]
+        protocol: restJson1
+        method: "POST"
+        bodyMediaType: "application/json"
+        uri: "/OperationWithNestedStructure"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "topLevel": {
+                    "dialog": {
+                        "language": "en"
+                    },
+                    "dialogList": [
+                        {
+                        },
+                        {
+                            "farewell": {}
+                        },
+                        {
+                            "language": "it",
+                            "greeting": "ciao",
+                            "farewell": {
+                                "phrase": "arrivederci"
+                            }
+                        }
+                    ],
+                    "dialogMap": {
+                        "emptyDialog": {
+                        },
+                        "partialEmptyDialog": {
+                            "language": "en",
+                            "farewell": {}
+                        },
+                        "nonEmptyDialog": {
+                            "greeting": "konnichiwa",
+                            "farewell": {
+                                "phrase": "sayonara"
+                            }
+                        }
+                    }
+                }
+            }"""
+        params: {
+            "topLevel": {
+                "dialog": {
+                    "language": "en"
+                    "greeting": "hi",
+                }
+                "dialogList": [
+                    {
+                        "greeting": "hi",
+                    },
+                    {
+                        "greeting": "hi",
+                        "farewell": {
+                            "phrase": "bye",
+                        }
+                    },
+                    {
+                        "language": "it",
+                        "greeting": "ciao",
+                        "farewell": {
+                            "phrase": "arrivederci"
+                        }
+                    }
+                ],
+                "dialogMap": {
+                    "emptyDialog": {
+                        "greeting": "hi",
+                    },
+                    "partialEmptyDialog": {
+                        "language": "en",
+                        "greeting": "hi",
+                        "farewell": {
+                            "phrase": "bye",
+                        }
+                    },
+                    "nonEmptyDialog": {
+                        "greeting": "konnichiwa",
+                        "farewell": {
+                            "phrase": "sayonara"
+                        }
+                    }
+                }
+            }
+        }
+    }
+])
+
+apply OperationWithNestedStructure @httpResponseTests([
+    {
+        id: "RestJsonClientPopulatesNestedDefaultsWhenMissingInResponseBody"
+        documentation: "Client populates nested default values when missing in response body."
+        appliesTo: "client"
+        tags: ["defaults"]
+        protocol: restJson1
+        code: 200
+        bodyMediaType: "application/json"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "dialog": {
+                    "language": "en"
+                },
+                "dialogList": [
+                    {
+                    },
+                    {
+                        "farewell": {}
+                    },
+                    {
+                        "language": "it",
+                        "greeting": "ciao",
+                        "farewell": {
+                            "phrase": "arrivederci"
+                        }
+                    }
+                ],
+                "dialogMap": {
+                    "emptyDialog": {
+                    },
+                    "partialEmptyDialog": {
+                        "language": "en",
+                        "farewell": {}
+                    },
+                    "nonEmptyDialog": {
+                        "greeting": "konnichiwa",
+                        "farewell": {
+                            "phrase": "sayonara"
+                        }
+                    }
+                }
+            }"""
+        params: {
+            "dialog": {
+                "language": "en"
+                "greeting": "hi",
+            }
+            "dialogList": [
+                {
+                    "greeting": "hi",
+                },
+                {
+                    "greeting": "hi",
+                    "farewell": {
+                        "phrase": "bye",
+                    }
+                },
+                {
+                    "language": "it",
+                    "greeting": "ciao",
+                    "farewell": {
+                        "phrase": "arrivederci"
+                    }
+                }
+            ],
+            "dialogMap": {
+                "emptyDialog": {
+                    "greeting": "hi",
+                },
+                "partialEmptyDialog": {
+                    "language": "en",
+                    "greeting": "hi",
+                    "farewell": {
+                        "phrase": "bye",
+                    }
+                },
+                "nonEmptyDialog": {
+                    "greeting": "konnichiwa",
+                    "farewell": {
+                        "phrase": "sayonara"
+                    }
+                }
+            }
+        }
+    }
+    {
+        id: "RestJsonServerPopulatesNestedDefaultValuesWhenMissingInInResponseParams"
+        documentation: "Server populates nested default values when missing in response params."
+        appliesTo: "server"
+        tags: ["defaults"]
+        protocol: restJson1
+        code: 200
+        bodyMediaType: "application/json"
+        headers: {"Content-Type": "application/json"}
+        body: """
+            {
+                "dialog": {
+                    "language": "en",
+                    "greeting": "hi"
+                },
+                "dialogList": [
+                    {
+                        "greeting": "hi"
+                    },
+                    {
+                        "greeting": "hi",
+                        "farewell": {
+                            "phrase": "bye"
+                        }
+                    },
+                    {
+                        "language": "it",
+                        "greeting": "ciao",
+                        "farewell": {
+                            "phrase": "arrivederci"
+                        }
+                    }
+                ],
+                "dialogMap": {
+                    "emptyDialog": {
+                        "greeting": "hi"
+                    },
+                    "partialEmptyDialog": {
+                        "language": "en",
+                        "greeting": "hi",
+                        "farewell": {
+                            "phrase": "bye"
+                        }
+                    },
+                    "nonEmptyDialog": {
+                        "greeting": "konnichiwa",
+                        "farewell": {
+                            "phrase": "sayonara"
+                        }
+                    }
+                }
+            }"""
+        params: {
+            "dialog": {
+                "language": "en"
+            },
+            "dialogList": [
+                {
+                },
+                {
+                    "farewell": {}
+                },
+                {
+                    "language": "it",
+                    "greeting": "ciao",
+                    "farewell": {
+                        "phrase": "arrivederci"
+                    }
+                }
+            ],
+            "dialogMap": {
+                "emptyDialog": {
+                },
+                "partialEmptyDialog": {
+                    "language": "en",
+                    "farewell": {}
+                },
+                "nonEmptyDialog": {
+                    "greeting": "konnichiwa",
+                    "farewell": {
+                        "phrase": "sayonara"
+                    }
+                }
+            }
+        }
+    }
+])
+
+@http(uri: "/OperationWithNestedStructure", method: "POST")
+operation OperationWithNestedStructure {
+    input := {
+        @required
+        topLevel: TopLevel
+    }
+
+    output := with [NestedDefaultsMixin] {
+    }
+}
+
+structure TopLevel with [NestedDefaultsMixin] {
+
+}
+
+@mixin
+structure NestedDefaultsMixin {
+    @required
+    dialog: Dialog
+    dialogList: DialogList = []
+    dialogMap: DialogMap = {}
+}
+
+structure Dialog {
+    language: String
+    greeting: String = "hi"
+    farewell: Farewell
+}
+
+structure Farewell {
+    phrase: String = "bye"
+}
+
+list DialogList {
+    member: Dialog
+}
+
+map DialogMap {
+    key: String
+    value: Dialog
+}
\ No newline at end of file
diff --git a/smithy-aws-protocol-tests/model/restXml/content-type.smithy b/smithy-aws-protocol-tests/model/restXml/content-type.smithy
new file mode 100644
index 000000000..4dc9c578e
--- /dev/null
+++ b/smithy-aws-protocol-tests/model/restXml/content-type.smithy
@@ -0,0 +1,44 @@
+// This file defines test cases that test Content-Type headers.
+
+$version: "2.0"
+
+namespace aws.protocoltests.restxml
+
+use aws.protocols#restXml
+use aws.protocoltests.shared#DateTime
+use smithy.test#httpRequestTests
+
+/// The example tests how servers must support requests
+/// containing a `Content-Type` header with parameters.
+@http(uri: "/ContentTypeParameters", method: "PUT")
+operation ContentTypeParameters {
+    input: ContentTypeParametersInput,
+    output: ContentTypeParametersOutput
+}
+
+apply ContentTypeParameters @httpRequestTests([
+    {
+        id: "RestXmlMustSupportParametersInContentType",
+        documentation: "A server should ignore parameters added to the content type",
+        protocol: restXml,
+        method: "PUT",
+        headers: {
+            "Content-Type": "application/xml; charset=utf-8"
+        },
+        uri: "/ContentTypeParameters",
+        body: "<ContentTypeParametersInput><value>5</value></ContentTypeParametersInput>",
+        bodyMediaType: "application/xml",
+        params: {
+            value: 5,
+        },
+        appliesTo: "server"
+    }
+])
+
+@input
+structure ContentTypeParametersInput {
+    value: Integer,
+}
+
+@output
+structure ContentTypeParametersOutput {}
diff --git a/smithy-aws-protocol-tests/model/restXml/main.smithy b/smithy-aws-protocol-tests/model/restXml/main.smithy
index 348eb303c..3841d3129 100644
--- a/smithy-aws-protocol-tests/model/restXml/main.smithy
+++ b/smithy-aws-protocol-tests/model/restXml/main.smithy
@@ -105,6 +105,9 @@ service RestXml {
         FractionalSeconds,
 
         // requestCompression trait tests
-        PutWithContentEncoding
+        PutWithContentEncoding,
+
+        // Content-Type header tests
+        ContentTypeParameters,
     ]
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
index 091284703..779a5f7b3 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/NodeValidationVisitorTest.java
@@ -246,9 +246,9 @@ public class NodeValidationVisitorTest {
 
                 // http-date
                 {"ns.foo#HttpDate", "\"Tue, 29 Apr 2014 18:30:38 GMT\"", null},
-                {"ns.foo#HttpDate", "\"Tuesday, 29 April 2014 18:30:38 GMT\"", new String[] {"Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 7231 section-7.1.1.1. Found: Tuesday, 29 April 2014 18:30:38 GMT"}},
-                {"ns.foo#HttpDate", "\"Tue, 29 Apr 2014 18:30:38 PST\"", new String[] {"Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 7231 section-7.1.1.1. Found: Tue, 29 Apr 2014 18:30:38 PST"}},
-                {"ns.foo#HttpDate", "11", new String[] {"Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 7231 section-7.1.1.1. Found: number"}},
+                {"ns.foo#HttpDate", "\"Tuesday, 29 April 2014 18:30:38 GMT\"", new String[] {"Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 9110 section-5.6.7. Found: Tuesday, 29 April 2014 18:30:38 GMT"}},
+                {"ns.foo#HttpDate", "\"Tue, 29 Apr 2014 18:30:38 PST\"", new String[] {"Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 9110 section-5.6.7. Found: Tue, 29 Apr 2014 18:30:38 PST"}},
+                {"ns.foo#HttpDate", "11", new String[] {"Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 9110 section-5.6.7. Found: number"}},
 
                 // date-time
                 {"ns.foo#DateTime", "\"1985-04-12T23:20:50.52Z\"", null},
@@ -267,8 +267,8 @@ public class NodeValidationVisitorTest {
                         "0: Invalid string value, `1985-04-12T23:20:50.52-07:00`, provided for timestamp, `ns.foo#TimestampList$member`. Expected an RFC 3339 formatted timestamp (e.g., \"1985-04-12T23:20:50.52Z\")"
                 }},
                 {"ns.foo#TimestampList", "[123]", new String[] {"0: Expected a string value for a date-time timestamp (e.g., \"1985-04-12T23:20:50.52Z\")"}},
-                {"ns.foo#Structure4", "{\"httpDate\": 1234}", new String[] {"httpDate: Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 7231 section-7.1.1.1. Found: number"}},
-                {"ns.foo#Structure4", "{\"httpDateTarget\": 1234}", new String[] {"httpDateTarget: Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 7231 section-7.1.1.1. Found: number"}},
+                {"ns.foo#Structure4", "{\"httpDate\": 1234}", new String[] {"httpDate: Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 9110 section-5.6.7. Found: number"}},
+                {"ns.foo#Structure4", "{\"httpDateTarget\": 1234}", new String[] {"httpDateTarget: Invalid value provided for http-date formatted timestamp. Expected a string value that matches the IMF-fixdate production of RFC 9110 section-5.6.7. Found: number"}},
 
                 // timestamp member with no format.
                 {"ns.foo#TimestampListNoFormatTrait", "[123]", null},
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-header-tchar-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-header-tchar-validator.errors
index f1242ede6..b0650ee97 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-header-tchar-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-header-tchar-validator.errors
@@ -1,1 +1,1 @@
-[DANGER] ns.foo#SayHelloInput$myHeader: `Not valid!` is not a valid HTTP header field name according to section 3.2 of RFC 7230 | HttpHeaderTrait
+[DANGER] ns.foo#SayHelloInput$myHeader: `Not valid!` is not a valid HTTP header field name according to section 5.6.2 of RFC 9110 | HttpHeaderTrait
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/inconsistent-params.errors b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/inconsistent-params.errors
index 0ac035113..9af8d2d2a 100644
--- a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/inconsistent-params.errors
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/inconsistent-params.errors
@@ -4,9 +4,14 @@
 [WARNING] example#GetResourceInput$ResourceId: This shape applies a trait that is unstable: smithy.rules#contextParam | UnstableTrait
 [WARNING] example#GetAnotherResource: This shape applies a trait that is unstable: smithy.rules#staticContextParams | UnstableTrait
 [WARNING] example#GetResource: This shape applies a trait that is unstable: smithy.rules#staticContextParams | UnstableTrait
+[WARNING] example#GetResource: This shape applies a trait that is unstable: smithy.rules#operationContextParams | UnstableTrait.smithy.rules#operationContextParams
+[WARNING] example#GetAnotherResource: This shape applies a trait that is unstable: smithy.rules#operationContextParams | UnstableTrait.smithy.rules#operationContextParams
 [ERROR] example#GetResource: Inconsistent type for `InconsistentParamType` parameter | RuleSetParameter.StaticContextParams.InconsistentType
 [ERROR] example#FizzBuzz: Type mismatch for parameter `ParameterFoo` | RuleSetParameter.RuleSet.TypeMismatch
 [ERROR] example#FizzBuzz: Parameter `ExtraParameter` exists in ruleset but not in service model | RuleSetParameter.RuleSet.UnmatchedName
 [ERROR] example#FizzBuzz: Parameter `AnotherParameterBar` exists in service model but not in ruleset | RuleSetParameter.RuleSet.UnmatchedName
 [ERROR] example#FizzBuzz: Parameter `InconsistentParamType` exists in service model but not in ruleset | RuleSetParameter.RuleSet.UnmatchedName
 [ERROR] example#FizzBuzz: Parameter `ParamNotInRuleset` exists in service model but not in ruleset | RuleSetParameter.RuleSet.UnmatchedName
+[ERROR] example#FizzBuzz: Parameter `InconsistentOperactionContextParam` exists in service model but not in ruleset | RuleSetParameter.RuleSet.UnmatchedName
+[ERROR] example#FizzBuzz: Type mismatch for parameter `StringArrayParam` | RuleSetParameter.RuleSet.TypeMismatch
+[ERROR] example#GetResource: Inconsistent type for `InconsistentOperactionContextParam` parameter | RuleSetParameter.OperationContextParams.InconsistentType
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/inconsistent-params.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/inconsistent-params.smithy
index c3e9e39e4..4c64d3adc 100644
--- a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/inconsistent-params.smithy
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/inconsistent-params.smithy
@@ -5,6 +5,7 @@ namespace example
 use smithy.rules#clientContextParams
 use smithy.rules#contextParam
 use smithy.rules#endpointRuleSet
+use smithy.rules#operationContextParams
 use smithy.rules#staticContextParams
 
 @endpointRuleSet({
@@ -30,6 +31,10 @@ use smithy.rules#staticContextParams
             "type": "string",
             "documentation": "docs",
             "builtIn": "SDK::Endpoint"
+        },
+        "StringArrayParam": {
+            "type": "stringArray",
+            "documentation": "docs"
         }
     },
     "rules": []
@@ -47,6 +52,10 @@ service FizzBuzz {
     "ParamNotInRuleset": {value: "someValue"},
     "InconsistentParamType": {value: true}
 )
+@operationContextParams(
+    "StringArrayParam": {path: "ResourceId"},
+    "InconsistentOperactionContextParam": {path: "ResourceId"}
+)
 operation GetResource {
     input: GetResourceInput
 }
@@ -61,13 +70,21 @@ structure GetResourceInput {
     "ParamNotInRuleset": {value: "someOtherValue"},
     "InconsistentParamType": {value: "someValue"}
 )
+@operationContextParams(
+    "InconsistentOperactionContextParam": {path: "ListOfStrings[*]"}
+)
 operation GetAnotherResource {
     input: GetAnotherResourceInput
 }
 
 structure GetAnotherResourceInput {
     @contextParam(name: "AnotherParameterBar")
-    ResourceId: ResourceId
+    ResourceId: ResourceId,
+    ListOfStrings: ListOfStrings
+}
+
+list ListOfStrings {
+    member: String
 }
 
 string ResourceId