From 5d2d96e7ed803861e2fe7e3f86e7295e79334d2a Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 11:07:17 -0400 Subject: [PATCH 01/18] add support for requiresLength trait and Transfer-Encoding: Chunked --- .../Middlewares/ContentLengthMiddleware.swift | 15 +++++++++++-- .../HttpBindingProtocolGenerator.kt | 22 ++++++++++++++++++- .../middlewares/ContentLengthMiddleware.kt | 8 ++++--- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index d7e76b9ad..e14a36795 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -6,7 +6,14 @@ public struct ContentLengthMiddleware private let contentLengthHeaderName = "Content-Length" - public init() {} + private var requiresLength: Bool = false + + private var unsignedPayload: Bool = false + + public init(requiresLength: Bool = false, unsignedPayload: Bool = false) { + self.requiresLength = requiresLength + self.unsignedPayload = unsignedPayload + } public func handle(context: Context, input: MInput, @@ -22,8 +29,12 @@ public struct ContentLengthMiddleware case .stream(let stream): if let length = stream.length { input.headers.update(name: "Content-Length", value: String(length)) + } else if !requiresLength && unsignedPayload { + // only for HTTP/1.1 requests, need to check explicitly in future + input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { - input.headers.update(name: "Transfer-Encoded", value: "Chunked") + let errorMessage = unsignedPayload ? "operation requires length" : "sigv4 requires length" + throw StreamError.notSupported(errorMessage) } default: input.headers.update(name: "Content-Length", value: "0") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 0d1a7af38..ca80abf03 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -4,6 +4,7 @@ */ package software.amazon.smithy.swift.codegen.integration +import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.knowledge.HttpBinding import software.amazon.smithy.model.knowledge.HttpBindingIndex @@ -30,6 +31,7 @@ import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait import software.amazon.smithy.model.traits.HttpQueryParamsTrait import software.amazon.smithy.model.traits.HttpQueryTrait import software.amazon.smithy.model.traits.MediaTypeTrait +import software.amazon.smithy.model.traits.RequiresLengthTrait import software.amazon.smithy.model.traits.StreamingTrait import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.swift.codegen.ClientRuntimeTypes @@ -62,6 +64,7 @@ import software.amazon.smithy.swift.codegen.model.ShapeMetadata import software.amazon.smithy.swift.codegen.model.bodySymbol import software.amazon.smithy.swift.codegen.model.hasEventStreamMember import software.amazon.smithy.swift.codegen.model.hasTrait +import software.amazon.smithy.swift.codegen.model.findStreamingMember import software.amazon.smithy.utils.OptionalUtils import java.util.Optional import java.util.logging.Logger @@ -403,6 +406,23 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { return resolved } + private fun hasRequiresLengthTrait(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean { + if (op.input.isPresent) { + val inputShape = ctx.model.expectShape(op.input.get()) + val streamingMember = inputShape.findStreamingMember(ctx.model) + if (streamingMember != null) { + return streamingMember.isBlobShape + && streamingMember.hasTrait() + && streamingMember.hasTrait() + } + } + return false + } + + private fun hasUnsignedBody(op: OperationShape): Boolean { + return op.hasTrait() + } + override fun generateProtocolClient(ctx: ProtocolGenerator.GenerationContext) { val symbol = ctx.symbolProvider.toSymbol(ctx.service) ctx.delegator.useFileWriter("./${ctx.settings.moduleName}/${symbol.name}.swift") { writer -> @@ -433,7 +453,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { operationMiddleware.appendMiddleware(operation, ContentTypeMiddleware(ctx.model, ctx.symbolProvider, resolver.determineRequestContentType(operation))) operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx.model, ctx.symbolProvider)) - operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance)) + operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance, hasRequiresLengthTrait(ctx, operation), hasUnsignedBody(operation))) operationMiddleware.appendMiddleware(operation, DeserializeMiddleware(ctx.model, ctx.symbolProvider)) operationMiddleware.appendMiddleware(operation, LoggingMiddleware(ctx.model, ctx.symbolProvider)) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt index 22d70721f..b1925348e 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt @@ -9,7 +9,7 @@ import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable import software.amazon.smithy.swift.codegen.middleware.MiddlewareStep -class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boolean) : MiddlewareRenderable { +class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boolean, private val requiresLength: Boolean, private val unsignedPayload: Boolean) : MiddlewareRenderable { override val name = "ContentLengthMiddleware" @@ -25,11 +25,13 @@ class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boo val hasHttpBody = MiddlewareShapeUtils.hasHttpBody(model, op) if (hasHttpBody || alwaysIntercept) { writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N())", + "\$L.\$L.intercept(position: \$L, middleware: \$N(requiresLength: \$L, unsignedPayload: \$L))", operationStackName, middlewareStep.stringValue(), position.stringValue(), - ClientRuntimeTypes.Middleware.ContentLengthMiddleware + ClientRuntimeTypes.Middleware.ContentLengthMiddleware, + requiresLength, + unsignedPayload ) } } From 76ef96ec794c2073f8c9415915647517472cfa55 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 12:51:17 -0400 Subject: [PATCH 02/18] lint & update comments --- .../Http/Middlewares/ContentLengthMiddleware.swift | 2 +- .../integration/HttpBindingProtocolGenerator.kt | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index e14a36795..2614e53cd 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -30,7 +30,7 @@ public struct ContentLengthMiddleware if let length = stream.length { input.headers.update(name: "Content-Length", value: String(length)) } else if !requiresLength && unsignedPayload { - // only for HTTP/1.1 requests, need to check explicitly in future + // only for HTTP/1.1 requests, should be removed in all HTTP/2 requests by SDK input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { let errorMessage = unsignedPayload ? "operation requires length" : "sigv4 requires length" diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index ca80abf03..779a393e8 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -406,19 +406,26 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { return resolved } + // Checks for @requiresLength trait + // Returns true if the operation: + // - has a streaming member + // - is a blob shape + // - has @httpPayload trait + // - has @requiresLen trait private fun hasRequiresLengthTrait(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean { if (op.input.isPresent) { val inputShape = ctx.model.expectShape(op.input.get()) val streamingMember = inputShape.findStreamingMember(ctx.model) if (streamingMember != null) { - return streamingMember.isBlobShape - && streamingMember.hasTrait() - && streamingMember.hasTrait() + return streamingMember.isBlobShape && + streamingMember.hasTrait() && + streamingMember.hasTrait() } } return false } + // Checks for @unsignedPayload trait private fun hasUnsignedBody(op: OperationShape): Boolean { return op.hasTrait() } From 7aabffd9f31a67856c1dbf7a94cdf98a5450b13f Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 13:26:55 -0400 Subject: [PATCH 03/18] format HttpBindingProtocolGenerator.kt with ktlint plugin --- .../HttpBindingProtocolGenerator.kt | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 779a393e8..31663fa87 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -62,9 +62,9 @@ import software.amazon.smithy.swift.codegen.integration.serde.UnionEncodeGenerat import software.amazon.smithy.swift.codegen.middleware.OperationMiddlewareGenerator import software.amazon.smithy.swift.codegen.model.ShapeMetadata import software.amazon.smithy.swift.codegen.model.bodySymbol +import software.amazon.smithy.swift.codegen.model.findStreamingMember import software.amazon.smithy.swift.codegen.model.hasEventStreamMember import software.amazon.smithy.swift.codegen.model.hasTrait -import software.amazon.smithy.swift.codegen.model.findStreamingMember import software.amazon.smithy.utils.OptionalUtils import java.util.Optional import java.util.logging.Logger @@ -94,9 +94,8 @@ fun formatHeaderOrQueryValue( memberShape: MemberShape, location: HttpBinding.Location, bindingIndex: HttpBindingIndex, - defaultTimestampFormat: TimestampFormatTrait.Format + defaultTimestampFormat: TimestampFormatTrait.Format, ): Pair { - return when (val shape = ctx.model.expectShape(memberShape.target)) { is TimestampShape -> { val timestampFormat = bindingIndex.determineTimestampFormat(memberShape, location, defaultTimestampFormat) @@ -168,7 +167,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { writer.openBlock( "extension $symbolName: \$N {", "}", - SwiftTypes.Protocols.Encodable + SwiftTypes.Protocols.Encodable, ) { writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) @@ -289,7 +288,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { private fun generateCodingKeysForMembers( ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, - members: List + members: List, ) { codingKeysGenerator.generateCodingKeysForMembers(ctx, writer, members) } @@ -301,7 +300,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { val inputType = ctx.model.expectShape(operation.input.get()) var metadata = mapOf( Pair(ShapeMetadata.OPERATION_SHAPE, operation), - Pair(ShapeMetadata.SERVICE_VERSION, ctx.service.version) + Pair(ShapeMetadata.SERVICE_VERSION, ctx.service.version), ) shapesInfo.put(inputType, metadata) } @@ -339,7 +338,6 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { } private fun resolveShapesNeedingCodableConformance(ctx: ProtocolGenerator.GenerationContext): Set { - val topLevelOutputMembers = getHttpBindingOperations(ctx).flatMap { val outputShape = ctx.model.expectShape(it.output.get()) outputShape.members() @@ -393,7 +391,8 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { RelationshipType.LIST_MEMBER, RelationshipType.SET_MEMBER, RelationshipType.MAP_VALUE, - RelationshipType.UNION_MEMBER -> true + RelationshipType.UNION_MEMBER, + -> true else -> false } }.forEach { @@ -418,8 +417,8 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { val streamingMember = inputShape.findStreamingMember(ctx.model) if (streamingMember != null) { return streamingMember.isBlobShape && - streamingMember.hasTrait() && - streamingMember.hasTrait() + streamingMember.hasTrait() && + streamingMember.hasTrait() } } return false @@ -441,7 +440,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { serviceSymbol.name, defaultContentType, httpProtocolCustomizable, - operationMiddleware + operationMiddleware, ) clientGenerator.render() } @@ -490,7 +489,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { members: List, writer: SwiftWriter, defaultTimestampFormat: TimestampFormatTrait.Format, - path: String? = null + path: String? = null, ) protected abstract fun renderStructDecode( ctx: ProtocolGenerator.GenerationContext, @@ -498,7 +497,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { members: List, writer: SwiftWriter, defaultTimestampFormat: TimestampFormatTrait.Format, - path: String + path: String, ) protected abstract fun addProtocolSpecificMiddleware(ctx: ProtocolGenerator.GenerationContext, operation: OperationShape) @@ -514,11 +513,11 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { for (operation in topDownIndex.getContainedOperations(ctx.service)) { OptionalUtils.ifPresentOrElse( Optional.of(getProtocolHttpBindingResolver(ctx, defaultContentType).httpTrait(operation)::class.java), - { containedOperations.add(operation) } + { containedOperations.add(operation) }, ) { LOGGER.warning( "Unable to fetch $protocolName protocol request bindings for ${operation.id} because " + - "it does not have an http binding trait" + "it does not have an http binding trait", ) } } From 1a55da5a79c50b23642cf04cba7ad2c336d78529 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 13:34:46 -0400 Subject: [PATCH 04/18] remove Transfer-Encoding header from h2 requests --- .../Networking/Http/Middlewares/ContentLengthMiddleware.swift | 2 +- Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index 2614e53cd..b6ec0abce 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -30,7 +30,7 @@ public struct ContentLengthMiddleware if let length = stream.length { input.headers.update(name: "Content-Length", value: String(length)) } else if !requiresLength && unsignedPayload { - // only for HTTP/1.1 requests, should be removed in all HTTP/2 requests by SDK + // only for HTTP/1.1 requests, will be removed in all HTTP/2 requests input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { let errorMessage = unsignedPayload ? "operation requires length" : "sigv4 requires length" diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index d6c5febcc..dabb62df5 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift @@ -75,6 +75,9 @@ extension SdkHttpRequest { httpRequest.path = [endpoint.path, endpoint.queryItemString].compactMap { $0 }.joined(separator: "?") httpRequest.addHeaders(headers: headers.toHttpHeaders()) + // Remove the "Transfer-Encoding" header if it exists since h2 does not support it + httpRequest.headers.remove(name: "Transfer-Encoding") + // HTTP2Request used with manual writes hence we need to set the body to nil // so that CRT does not write the body for us (we will write it manually) httpRequest.body = nil From 86ae4b3d493515597585d9564234f089679eea01 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 13:44:06 -0400 Subject: [PATCH 05/18] force 20 spaces instead of 23 for lint and use removeHeader function --- Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift | 2 +- .../swift/codegen/integration/HttpBindingProtocolGenerator.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index dabb62df5..1c106cff9 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift @@ -76,7 +76,7 @@ extension SdkHttpRequest { httpRequest.addHeaders(headers: headers.toHttpHeaders()) // Remove the "Transfer-Encoding" header if it exists since h2 does not support it - httpRequest.headers.remove(name: "Transfer-Encoding") + httpRequest.removeHeader(name: "Transfer-Encoding") // HTTP2Request used with manual writes hence we need to set the body to nil // so that CRT does not write the body for us (we will write it manually) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 31663fa87..448b129dc 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -417,8 +417,8 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { val streamingMember = inputShape.findStreamingMember(ctx.model) if (streamingMember != null) { return streamingMember.isBlobShape && - streamingMember.hasTrait() && - streamingMember.hasTrait() + streamingMember.hasTrait() && + streamingMember.hasTrait() } } return false From 63aedcd7c7ac454453a33c5e46529d83b10fe996 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 13:46:57 -0400 Subject: [PATCH 06/18] update indentation, difference between CI and local --- .../swift/codegen/integration/HttpBindingProtocolGenerator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 448b129dc..7b65cda2d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -417,8 +417,8 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { val streamingMember = inputShape.findStreamingMember(ctx.model) if (streamingMember != null) { return streamingMember.isBlobShape && - streamingMember.hasTrait() && - streamingMember.hasTrait() + streamingMember.hasTrait() && + streamingMember.hasTrait() } } return false From 58566bc811cefb4c286ed93ff6991a17b7cf167f Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 16:33:16 -0400 Subject: [PATCH 07/18] update ContentLengthMiddleware.kt logic --- .../middlewares/ContentLengthMiddleware.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt index b1925348e..907ecfc67 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt @@ -20,19 +20,20 @@ class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boo override fun render( writer: SwiftWriter, op: OperationShape, - operationStackName: String + operationStackName: String, ) { val hasHttpBody = MiddlewareShapeUtils.hasHttpBody(model, op) if (hasHttpBody || alwaysIntercept) { - writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N(requiresLength: \$L, unsignedPayload: \$L))", - operationStackName, - middlewareStep.stringValue(), - position.stringValue(), - ClientRuntimeTypes.Middleware.ContentLengthMiddleware, - requiresLength, - unsignedPayload - ) + val middlewareArgs = if (requiresLength || unsignedPayload) { + "requiresLength: $requiresLength, unsignedPayload: $unsignedPayload" + } else { + "" + } + + val interceptStatement = "$operationStackName.${middlewareStep.stringValue()}.intercept(" + + "position: ${position.stringValue()}, middleware: ${ClientRuntimeTypes.Middleware.ContentLengthMiddleware}($middlewareArgs))" + + writer.write(interceptStatement) } } } From de7476c8968311ccaff34a70b8661d00d503e5ba Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 16:39:08 -0400 Subject: [PATCH 08/18] lint issue again --- .../codegen/integration/middlewares/ContentLengthMiddleware.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt index 907ecfc67..d620171e9 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt @@ -31,7 +31,7 @@ class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boo } val interceptStatement = "$operationStackName.${middlewareStep.stringValue()}.intercept(" + - "position: ${position.stringValue()}, middleware: ${ClientRuntimeTypes.Middleware.ContentLengthMiddleware}($middlewareArgs))" + "position: ${position.stringValue()}, middleware: ${ClientRuntimeTypes.Middleware.ContentLengthMiddleware}($middlewareArgs))" writer.write(interceptStatement) } From 58a1b711e636fbdb1cd158d891743875f0e471aa Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 18 Oct 2023 17:33:04 -0400 Subject: [PATCH 09/18] add tests for ContentLengthMiddleware.swift logic --- .../ContentLengthMiddlewareTests.swift | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift new file mode 100644 index 000000000..f21b80738 --- /dev/null +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -0,0 +1,80 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import XCTest +import SmithyTestUtil +@testable import ClientRuntime + +class ContentLengthMiddlewareTests: XCTestCase { + private var builtContext: HttpContext! + private var stack: OperationStack! + + override func setUpWithError() throws { + try super.setUpWithError() + builtContext = HttpContextBuilder() + .withMethod(value: .get) + .withPath(value: "/") + .withEncoder(value: JSONEncoder()) + .withDecoder(value: JSONDecoder()) + .withOperation(value: "Test Operation") + .build() + stack = OperationStack(id: "Test Operation") + } + + func testTransferEncodingChunkedSetWhenStreamLengthIsNil() async throws { + addContentLengthMiddlewareWith(requiresLength: false, unsignedPayload: true) + forceEmptyStream() + try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "Chunked"]) + } + + func testContentLengthSetWhenStreamLengthAvailableAndRequiresLengthSet() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: false) + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + } + + func testRequiresLengthSetWithNilStreamShouldThrowError() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: false) + forceEmptyStream() + do { + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + XCTFail("Should throw error") + } catch let error as StreamError { + switch error { + case .notSupported("sigv4 requires length"), .notSupported("operation requires length"): + // The error matches one of the expected cases, test passes + break + default: + XCTFail("Error is not StreamError.notSupported with expected message") + } + } + } + + private func addContentLengthMiddlewareWith(requiresLength: Bool, unsignedPayload: Bool) { + stack.finalizeStep.intercept( + position: .before, + middleware: ContentLengthMiddleware(requiresLength: requiresLength, unsignedPayload: unsignedPayload) + ) + } + + private func forceEmptyStream() { + // Force stream length to be nil + stack.finalizeStep.intercept(position: .before, id: "set nil stream length") { (context, input, next) -> OperationOutput in + input.body = .stream(BufferedStream()) // Set the stream length to nil + return try await next.handle(context: context, input: input) + } + } + + private func AssertHeadersArePresent(expectedHeaders: [String: String]) async throws -> Void { + _ = try await stack.handleMiddleware(context: builtContext, input: MockInput(), + next: MockHandler(handleCallback: { (_, input) in + for (key, value) in expectedHeaders { + XCTAssert(input.headers.value(for: key) == value) + } + let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) + let output = OperationOutput(httpResponse: httpResponse, + output: mockOutput) + return output + })) + } +} \ No newline at end of file From 408655ca10fa31c4b7bb90eba735af8f61613180 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 19 Oct 2023 14:37:36 -0400 Subject: [PATCH 10/18] update logic for detecthing requiresLength and add codegen tests --- .../ContentLengthMiddlewareTests.swift | 5 + .../HttpBindingProtocolGenerator.kt | 17 +-- .../HttpBindingProtocolGeneratorTests.kt | 3 +- .../HttpProtocolClientGeneratorTests.kt | 102 ++++++++++++++++++ .../service-generator-test-operations.smithy | 36 ++++++- 5 files changed, 153 insertions(+), 10 deletions(-) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index f21b80738..2e548992e 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -32,6 +32,11 @@ class ContentLengthMiddlewareTests: XCTestCase { try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) } + func testContentLengthSetWhenRequiresLengthAndUnsignedPayload() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: true) + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + } + func testRequiresLengthSetWithNilStreamShouldThrowError() async throws { addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: false) forceEmptyStream() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 7b65cda2d..83f773d32 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -407,24 +407,25 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { // Checks for @requiresLength trait // Returns true if the operation: - // - has a streaming member - // - is a blob shape - // - has @httpPayload trait - // - has @requiresLen trait + // - has a streaming member with @httpPayload trait + // - target is a blob shape with @requiresLength trait private fun hasRequiresLengthTrait(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean { if (op.input.isPresent) { val inputShape = ctx.model.expectShape(op.input.get()) val streamingMember = inputShape.findStreamingMember(ctx.model) if (streamingMember != null) { - return streamingMember.isBlobShape && - streamingMember.hasTrait() && - streamingMember.hasTrait() + val targetShape = ctx.model.expectShape(streamingMember.target) + if (targetShape != null) { + return streamingMember.hasTrait() && + targetShape.isBlobShape && + targetShape.hasTrait() + } } } return false } - // Checks for @unsignedPayload trait + // Checks for @unsignedPayload trait on an operation private fun hasUnsignedBody(op: OperationShape): Boolean { return op.hasTrait() } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt index 46e608384..4f996a72b 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt @@ -38,7 +38,7 @@ class TestHttpProtocolClientGeneratorFactory : HttpProtocolClientGeneratorFactor private fun getClientProperties(ctx: ProtocolGenerator.GenerationContext): List { return mutableListOf( DefaultRequestEncoder(), - DefaultResponseDecoder() + DefaultResponseDecoder(), ) } @@ -125,6 +125,7 @@ extension InlineDocumentAsPayloadOutput: ClientRuntime.HttpResponseBinding { """.trimIndent() contents.shouldContainOnlyOnce(expectedContents) } + @Test fun `default fooMap to an empty map if keysForFooMap is empty`() { val contents = getModelFileContents("example", "HttpPrefixHeadersOutput+HttpResponseBinding.swift", newTestContext.manifest) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index 1e46c20a7..cc669d7dd 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -148,6 +148,108 @@ class HttpProtocolClientGeneratorTests { contents.shouldContainOnlyOnce(expected) } + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength false and unsignedPayload true`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = """ + public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStream") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "GetFooStreamingRequest")) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: false, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() + contents.shouldContainOnlyOnce(expected) + } + + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength true and unsignedPayload false`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = """ + public func explicitBlobStreamWithLength(input: ExplicitBlobStreamWithLengthInput) async throws -> ExplicitBlobStreamWithLengthOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "explicitBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "explicitBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) + operation.serializeStep.intercept(position: .after, middleware: ExplicitBlobStreamWithLengthInputBodyMiddleware()) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: false)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() + contents.shouldContainOnlyOnce(expected) + } + + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength true and unsignedPayload true`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) + operation.serializeStep.intercept(position: .after, middleware: UnsignedFooBlobStreamWithLengthInputBodyMiddleware()) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() + contents.shouldContainOnlyOnce(expected) + } + private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { val context = TestContext.initContextFrom(smithyFile, serviceShapeId, MockHttpRestJsonProtocolGenerator()) { model -> model.defaultSettings(serviceShapeId, "RestJson", "2019-12-16", "Rest Json Protocol") diff --git a/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy b/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy index 537e5531c..f1a6f4e71 100644 --- a/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy +++ b/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy @@ -2,6 +2,7 @@ $version: "1.0" namespace com.test use aws.protocols#awsJson1_1 +use aws.auth#unsignedPayload @awsJson1_1 service Example { @@ -15,7 +16,10 @@ service Example { GetFooStreamingOutputNoInput, GetFooStreamingInputNoOutput, AllocateWidget, - OperationWithDeprecatedTrait + OperationWithDeprecatedTrait, + UnsignedFooBlobStream, + UnsignedFooBlobStreamWithLength, + ExplicitBlobStreamWithLength ] } @@ -89,4 +93,34 @@ operation AllocateWidget { structure AllocateWidgetInput { @idempotencyToken clientToken: String +} + +// Stream must have a known size +@streaming +@requiresLength +blob BodyStreamWithLength + +@http(method: "POST", uri: "/explicit/blobstreamunsigned") +@unsignedPayload +operation UnsignedFooBlobStream { + input: GetFooStreamingRequest, + output: GetFooStreamingResponse +} + +@http(method: "POST", uri: "/explicit/blobstreamunsignedwithlength") +@unsignedPayload +operation UnsignedFooBlobStreamWithLength { + input: ExplicitBlobStreamWithLengthRequest, + output: GetFooStreamingResponse +} + +@http(method: "POST", uri: "/explicit/blobstreamwithlength") +operation ExplicitBlobStreamWithLength { + input: ExplicitBlobStreamWithLengthRequest, + output: GetFooStreamingResponse +} + +structure ExplicitBlobStreamWithLengthRequest { + @httpPayload + payload1: BodyStreamWithLength } \ No newline at end of file From 142e9d7808f2f8222d676880662f2d6689141bd0 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 19 Oct 2023 14:49:41 -0400 Subject: [PATCH 11/18] lint issue again --- .../swift/codegen/integration/HttpBindingProtocolGenerator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 83f773d32..638fd984f 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -417,8 +417,8 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { val targetShape = ctx.model.expectShape(streamingMember.target) if (targetShape != null) { return streamingMember.hasTrait() && - targetShape.isBlobShape && - targetShape.hasTrait() + targetShape.isBlobShape && + targetShape.hasTrait() } } } From c464ab67078c8e08d6a177362927f590832ad678 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 19 Oct 2023 15:45:07 -0400 Subject: [PATCH 12/18] fix spacing in codegen tests --- .../HttpProtocolClientGeneratorTests.kt | 159 +++++++++--------- 1 file changed, 81 insertions(+), 78 deletions(-) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index cc669d7dd..208f816c6 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -153,32 +153,33 @@ class HttpProtocolClientGeneratorTests { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() - val expected = """ - public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .post) - .withServiceName(value: serviceName) - .withOperation(value: "unsignedFooBlobStream") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "GetFooStreamingRequest")) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: false, unsignedPayload: true)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - """.trimIndent() + val expected = + """ + public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStream") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "GetFooStreamingRequest")) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: false, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() contents.shouldContainOnlyOnce(expected) } @@ -187,32 +188,33 @@ class HttpProtocolClientGeneratorTests { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() - val expected = """ - public func explicitBlobStreamWithLength(input: ExplicitBlobStreamWithLengthInput) async throws -> ExplicitBlobStreamWithLengthOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .post) - .withServiceName(value: serviceName) - .withOperation(value: "explicitBlobStreamWithLength") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "explicitBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) - operation.serializeStep.intercept(position: .after, middleware: ExplicitBlobStreamWithLengthInputBodyMiddleware()) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: false)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - """.trimIndent() + val expected = + """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) + operation.serializeStep.intercept(position: .after, middleware: UnsignedFooBlobStreamWithLengthInputBodyMiddleware()) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() contents.shouldContainOnlyOnce(expected) } @@ -221,32 +223,33 @@ class HttpProtocolClientGeneratorTests { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() - val expected = """ - public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .post) - .withServiceName(value: serviceName) - .withOperation(value: "unsignedFooBlobStreamWithLength") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) - operation.serializeStep.intercept(position: .after, middleware: UnsignedFooBlobStreamWithLengthInputBodyMiddleware()) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - """.trimIndent() + val expected = + """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) + operation.serializeStep.intercept(position: .after, middleware: UnsignedFooBlobStreamWithLengthInputBodyMiddleware()) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() contents.shouldContainOnlyOnce(expected) } From 381e52cd4c5e7a93dc8eab746ddc0c4156e0910f Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 19 Oct 2023 16:28:50 -0400 Subject: [PATCH 13/18] pass file and line in custom assert --- .../NetworkingTests/ContentLengthMiddlewareTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index 2e548992e..7611d98f0 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -69,11 +69,11 @@ class ContentLengthMiddlewareTests: XCTestCase { } } - private func AssertHeadersArePresent(expectedHeaders: [String: String]) async throws -> Void { + private func AssertHeadersArePresent(expectedHeaders: [String: String], file: StaticString = #file, line: UInt = #line) async throws -> Void { _ = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: MockHandler(handleCallback: { (_, input) in for (key, value) in expectedHeaders { - XCTAssert(input.headers.value(for: key) == value) + XCTAssert(input.headers.value(for: key) == value, file: file, line: line) } let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) From 80aba8954b57360db3d68b5e8972875c28392bcb Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 20 Oct 2023 10:55:57 -0400 Subject: [PATCH 14/18] addresss PR comments --- .../ContentLengthMiddlewareTests.swift | 22 +++++++++---------- .../HttpBindingProtocolGenerator.kt | 6 ++--- .../middlewares/ContentLengthMiddleware.kt | 7 ++---- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index 7611d98f0..363233630 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -70,16 +70,16 @@ class ContentLengthMiddlewareTests: XCTestCase { } private func AssertHeadersArePresent(expectedHeaders: [String: String], file: StaticString = #file, line: UInt = #line) async throws -> Void { - _ = try await stack.handleMiddleware(context: builtContext, input: MockInput(), - next: MockHandler(handleCallback: { (_, input) in - for (key, value) in expectedHeaders { - XCTAssert(input.headers.value(for: key) == value, file: file, line: line) - } - let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) - let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) - let output = OperationOutput(httpResponse: httpResponse, - output: mockOutput) - return output - })) + let mockHandler = MockHandler { (_, input) in + for (key, value) in expectedHeaders { + XCTAssert(input.headers.value(for: key) == value, file: file, line: line) + } + let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) + let output = OperationOutput(httpResponse: httpResponse, output: mockOutput) + return output + } + + _ = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: mockHandler) } } \ No newline at end of file diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 638fd984f..cefe2bb5d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -426,9 +426,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { } // Checks for @unsignedPayload trait on an operation - private fun hasUnsignedBody(op: OperationShape): Boolean { - return op.hasTrait() - } + private fun hasUnsignedPayloadTrait(op: OperationShape): Boolean = op.hasTrait() override fun generateProtocolClient(ctx: ProtocolGenerator.GenerationContext) { val symbol = ctx.symbolProvider.toSymbol(ctx.service) @@ -460,7 +458,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { operationMiddleware.appendMiddleware(operation, ContentTypeMiddleware(ctx.model, ctx.symbolProvider, resolver.determineRequestContentType(operation))) operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx.model, ctx.symbolProvider)) - operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance, hasRequiresLengthTrait(ctx, operation), hasUnsignedBody(operation))) + operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance, hasRequiresLengthTrait(ctx, operation), hasUnsignedPayloadTrait(operation))) operationMiddleware.appendMiddleware(operation, DeserializeMiddleware(ctx.model, ctx.symbolProvider)) operationMiddleware.appendMiddleware(operation, LoggingMiddleware(ctx.model, ctx.symbolProvider)) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt index d620171e9..349597156 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt @@ -24,11 +24,8 @@ class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boo ) { val hasHttpBody = MiddlewareShapeUtils.hasHttpBody(model, op) if (hasHttpBody || alwaysIntercept) { - val middlewareArgs = if (requiresLength || unsignedPayload) { - "requiresLength: $requiresLength, unsignedPayload: $unsignedPayload" - } else { - "" - } + val str = "requiresLength: $requiresLength, unsignedPayload: $unsignedPayload" + val middlewareArgs = str.takeIf {requiresLength || unsignedPayload} ?: "" val interceptStatement = "$operationStackName.${middlewareStep.stringValue()}.intercept(" + "position: ${position.stringValue()}, middleware: ${ClientRuntimeTypes.Middleware.ContentLengthMiddleware}($middlewareArgs))" From 63601c5b2c974590b5e966acf6ab9d0f746f3321 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 20 Oct 2023 10:59:06 -0400 Subject: [PATCH 15/18] fix spacing --- .../codegen/integration/middlewares/ContentLengthMiddleware.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt index 349597156..af6975535 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt @@ -25,7 +25,7 @@ class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boo val hasHttpBody = MiddlewareShapeUtils.hasHttpBody(model, op) if (hasHttpBody || alwaysIntercept) { val str = "requiresLength: $requiresLength, unsignedPayload: $unsignedPayload" - val middlewareArgs = str.takeIf {requiresLength || unsignedPayload} ?: "" + val middlewareArgs = str.takeIf { requiresLength || unsignedPayload } ?: "" val interceptStatement = "$operationStackName.${middlewareStep.stringValue()}.intercept(" + "position: ${position.stringValue()}, middleware: ${ClientRuntimeTypes.Middleware.ContentLengthMiddleware}($middlewareArgs))" From 204d8177aaa5c53bacb1a4908878c12ddadd8a6d Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 20 Oct 2023 16:18:54 -0400 Subject: [PATCH 16/18] add better error messaging --- .../Http/Middlewares/ContentLengthMiddleware.swift | 5 ++++- .../ContentLengthMiddlewareTests.swift | 12 ++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index b6ec0abce..e508c634c 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -33,7 +33,10 @@ public struct ContentLengthMiddleware // only for HTTP/1.1 requests, will be removed in all HTTP/2 requests input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { - let errorMessage = unsignedPayload ? "operation requires length" : "sigv4 requires length" + let operation = context.attributes.get(key: AttributeKey(name: "Operation")) + let errorMessage = unsignedPayload ? + "Missing content-length for operation: \(operation)" : + "Missing content-length for SigV4 signing on operation: \(operation)" throw StreamError.notSupported(errorMessage) } default: diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index 363233630..86505905c 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -43,14 +43,10 @@ class ContentLengthMiddlewareTests: XCTestCase { do { try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) XCTFail("Should throw error") - } catch let error as StreamError { - switch error { - case .notSupported("sigv4 requires length"), .notSupported("operation requires length"): - // The error matches one of the expected cases, test passes - break - default: - XCTFail("Error is not StreamError.notSupported with expected message") - } + } catch let error as StreamError where error.localizedDescription.contains("requires length") { + // The error matches the expected case, test passes + } catch { + XCTFail("Error is not StreamError with the expected message") } } From 415c19b86e36456748a63b827374543952fac082 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 20 Oct 2023 16:30:43 -0400 Subject: [PATCH 17/18] fix tests --- .../Http/Middlewares/ContentLengthMiddleware.swift | 2 +- .../ContentLengthMiddlewareTests.swift | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index e508c634c..dbcaaebdb 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -33,7 +33,7 @@ public struct ContentLengthMiddleware // only for HTTP/1.1 requests, will be removed in all HTTP/2 requests input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { - let operation = context.attributes.get(key: AttributeKey(name: "Operation")) + let operation = context.attributes.get(key: AttributeKey(name: "Operation")) ?? "Error getting operation name" let errorMessage = unsignedPayload ? "Missing content-length for operation: \(operation)" : "Missing content-length for SigV4 signing on operation: \(operation)" diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index 86505905c..3e5157523 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -43,10 +43,14 @@ class ContentLengthMiddlewareTests: XCTestCase { do { try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) XCTFail("Should throw error") - } catch let error as StreamError where error.localizedDescription.contains("requires length") { - // The error matches the expected case, test passes - } catch { - XCTFail("Error is not StreamError with the expected message") + } catch let error as StreamError { + switch error { + case .notSupported("Missing content-length for SigV4 signing on operation: Test Operation"), .notSupported("Missing content-length for operation: Test Operation"): + // The error matches one of the expected cases, test passes + break + default: + XCTFail("Error is not StreamError.notSupported with expected message") + } } } From 7244fbf9b7cf1c258dccf327a2f8b4a1677efa2a Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 20 Oct 2023 17:33:33 -0400 Subject: [PATCH 18/18] fix line is too long --- .../Networking/Http/Middlewares/ContentLengthMiddleware.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index dbcaaebdb..6fc759e95 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -33,7 +33,8 @@ public struct ContentLengthMiddleware // only for HTTP/1.1 requests, will be removed in all HTTP/2 requests input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { - let operation = context.attributes.get(key: AttributeKey(name: "Operation")) ?? "Error getting operation name" + let operation = context.attributes.get(key: AttributeKey(name: "Operation")) + ?? "Error getting operation name" let errorMessage = unsignedPayload ? "Missing content-length for operation: \(operation)" : "Missing content-length for SigV4 signing on operation: \(operation)"