Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle errors in 200 response from S3 #1266

Merged
merged 11 commits into from
Jan 4, 2024
55 changes: 55 additions & 0 deletions Tests/Services/AWSS3Tests/S3ErrorIn200Test.swift
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the files in Tests/Services/* get deleted & re-generated every time codegen is performed. I think we need a different place to put this file.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Code generated by smithy-swift-codegen. DO NOT EDIT!

import AWSClientRuntime
@testable import AWSS3
import AwsCommonRuntimeKit
import ClientRuntime
import SmithyTestUtil
import XCTest

public class MockHttpClientEngine: HttpClientEngine {

// Public initializer
public init() {}

func successHttpResponse(request: SdkHttpRequest) -> HttpResponse {
let errorResponsePayload = """
<Error>
<Code>SlowDown</Code>
<Message>Please reduce your request rate.</Message>
<RequestId>K2H6N7ZGQT6WHCEG</RequestId>
<HostId>WWoZlnK4pTjKCYn6eNV7GgOurabfqLkjbSyqTvDMGBaI9uwzyNhSaDhOCPs8paFGye7S6b/AB3A=</HostId>
</Error>
"""
return HttpResponse(headers: request.headers, body: ByteStream.data(errorResponsePayload.data(using: .utf8)), statusCode: HttpStatusCode.ok)
}

public func execute(request: SdkHttpRequest) async throws -> HttpResponse {
return successHttpResponse(request: request)
}
}

class S3ErrorIn200Test: XCTestCase {

override class func setUp() {
AwsCommonRuntimeKit.CommonRuntimeKit.initialize()
}

/// S3Client throws expected error in response (200) with <Error> tag
func testFoundExpectedError() async throws {
let config = try await S3Client.S3ClientConfiguration(region: "us-west-2")
config.httpClientEngine = MockHttpClientEngine()
let client = S3Client(config: config)

do {
// any method on S3Client where the output shape doesnt have a stream
_ = try await client.listBuckets(input: .init())
XCTFail("Expected an error to be thrown, but it was not.")
} catch let error as UnknownAWSHTTPServiceError {
// check for the error we added in our mock client
XCTAssertEqual("Please reduce your request rate.", error.message)
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.aws.swift.codegen.customization.s3

import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.traits.StreamingTrait
import software.amazon.smithy.swift.codegen.ClientRuntimeTypes
import software.amazon.smithy.swift.codegen.SwiftSettings
import software.amazon.smithy.swift.codegen.SwiftWriter
import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator
import software.amazon.smithy.swift.codegen.integration.SwiftIntegration
import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils
import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition
import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable
import software.amazon.smithy.swift.codegen.middleware.MiddlewareStep
import software.amazon.smithy.swift.codegen.middleware.OperationMiddleware
import software.amazon.smithy.swift.codegen.model.expectShape
import software.amazon.smithy.swift.codegen.model.hasTrait

/**
* Register interceptor to handle S3 error responses returned with an HTTP 200 status code.
* see [aws-sdk-kotlin#199](https://github.com/awslabs/aws-sdk-kotlin/issues/199)
* see [aws-sdk-swift#1113](https://github.com/awslabs/aws-sdk-swift/issues/1113)
*/
class S3ErrorWith200StatusIntegration : SwiftIntegration {
override fun enabledForService(model: Model, settings: SwiftSettings): Boolean =
model.expectShape<ServiceShape>(settings.service).isS3

override fun customizeMiddleware(
ctx: ProtocolGenerator.GenerationContext,
operationShape: OperationShape,
operationMiddleware: OperationMiddleware,
) {
// we don't know for sure what operations S3 does this on. Go customized this for only a select few
// like CopyObject/UploadPartCopy/CompleteMultipartUpload but Rust hit it on additional operations
// (DeleteObjects).
// Instead of playing whack-a-mole broadly apply this interceptor to everything but streaming responses
// which adds a small amount of overhead to response processing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with this. I don't think it will cause any problems where it is not needed.

val output = ctx.model.expectShape(operationShape.output.get())
val outputIsNotStreaming = output.members().none {
it.hasTrait<StreamingTrait>() || ctx.model.expectShape(it.target).hasTrait<StreamingTrait>()
}
if (outputIsNotStreaming) {
operationMiddleware.appendMiddleware(operationShape, S3HandleError200ResponseMiddleware)
}
}
}

private object S3HandleError200ResponseMiddleware : MiddlewareRenderable {
override val name = "S3ErrorWith200StatusXMLMiddleware"

override val middlewareStep = MiddlewareStep.DESERIALIZESTEP

override val position = MiddlewarePosition.AFTER

override fun render(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String) {
val outputShape = MiddlewareShapeUtils.outputSymbol(ctx.symbolProvider, ctx.model, op)
writer.openBlock(
"$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, id: \"${name}\") { (context, input, next) -> \$N<${outputShape.name}> in",
"}",
ClientRuntimeTypes.Middleware.OperationOutput,
) {
writer.apply {
// Send the request and get the response
write("let response = try await next.handle(context: context, input: input)")

// Check if the response status is 200
write("guard response.httpResponse.statusCode == .ok else {")
write(" return try await next.handle(context: context, input: input)")
write("}")

// Read the response body
write("guard let data = try await response.httpResponse.body.readData() else {")
write(" return try await next.handle(context: context, input: input)")
write("}")
write("let xmlString = String(data: data, encoding: .utf8) ?? \"\"")

// Check for <Error> tag in the XML
write("if xmlString.contains(\"<Error>\") {")
write(" // Handle the error as a 500 Internal Server Error")
write(" response.httpResponse.statusCode = .internalServerError")
write(" return response")
write("}")

write("return try await next.handle(context: context, input: input)")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
software.amazon.smithy.aws.swift.codegen.AddProtocols
software.amazon.smithy.aws.swift.codegen.customization.s3.S3ErrorIntegration
software.amazon.smithy.aws.swift.codegen.customization.s3.S3ErrorWith200StatusIntegration
software.amazon.smithy.aws.swift.codegen.customization.s3.S3Expires
software.amazon.smithy.aws.swift.codegen.customization.s3.TruncatablePaginationIntegration
software.amazon.smithy.aws.swift.codegen.customization.route53.Route53TrimHostedZone
Expand Down
Loading