diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt index 52381ed5c82..e6ef8163318 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerCodegenVisitor.kt @@ -118,7 +118,7 @@ open class ServerCodegenVisitor( val baseModel = baselineTransform(context.model) val service = settings.getService(baseModel) - val (protocol, generator) = + val (protocolShape, protocolGeneratorFactory) = ServerProtocolLoader( codegenDecorator.protocols( service.id, @@ -126,7 +126,7 @@ open class ServerCodegenVisitor( ), ) .protocolFor(context.model, service) - protocolGeneratorFactory = generator + this.protocolGeneratorFactory = protocolGeneratorFactory model = codegenDecorator.transformModel(service, baseModel) @@ -145,7 +145,7 @@ open class ServerCodegenVisitor( serverSymbolProviders.symbolProvider, null, service, - protocol, + protocolShape, settings, serverSymbolProviders.unconstrainedShapeSymbolProvider, serverSymbolProviders.constrainedShapeSymbolProvider, @@ -169,7 +169,7 @@ open class ServerCodegenVisitor( settings.codegenConfig, codegenContext.expectModuleDocProvider(), ) - protocolGenerator = protocolGeneratorFactory.buildProtocolGenerator(codegenContext) + protocolGenerator = this.protocolGeneratorFactory.buildProtocolGenerator(codegenContext) } /** @@ -315,7 +315,12 @@ open class ServerCodegenVisitor( writer: RustWriter, ) { if (codegenContext.settings.codegenConfig.publicConstrainedTypes || shape.isReachableFromOperationInput()) { - val serverBuilderGenerator = ServerBuilderGenerator(codegenContext, shape, validationExceptionConversionGenerator) + val serverBuilderGenerator = ServerBuilderGenerator( + codegenContext, + shape, + validationExceptionConversionGenerator, + protocolGenerator.protocol, + ) serverBuilderGenerator.render(rustCrate, writer) if (codegenContext.settings.codegenConfig.publicConstrainedTypes) { @@ -336,7 +341,12 @@ open class ServerCodegenVisitor( if (!codegenContext.settings.codegenConfig.publicConstrainedTypes) { val serverBuilderGeneratorWithoutPublicConstrainedTypes = - ServerBuilderGeneratorWithoutPublicConstrainedTypes(codegenContext, shape, validationExceptionConversionGenerator) + ServerBuilderGeneratorWithoutPublicConstrainedTypes( + codegenContext, + shape, + validationExceptionConversionGenerator, + protocolGenerator.protocol, + ) serverBuilderGeneratorWithoutPublicConstrainedTypes.render(rustCrate, writer) writer.implBlock(codegenContext.symbolProvider.toSymbol(shape)) { diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRuntimeType.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRuntimeType.kt index e31275fa333..8165887871e 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRuntimeType.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/ServerRuntimeType.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.server.smithy import software.amazon.smithy.rust.codegen.core.rustlang.InlineDependency import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol /** * Object used *exclusively* in the runtime of the server, for separation concerns. @@ -15,17 +16,11 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType * For a runtime type that is used in the client, or in both the client and the server, use [RuntimeType] directly. */ object ServerRuntimeType { - fun forInlineDependency(inlineDependency: InlineDependency) = RuntimeType("crate::${inlineDependency.name}", inlineDependency) + fun router(runtimeConfig: RuntimeConfig) = + ServerCargoDependency.smithyHttpServer(runtimeConfig).toType().resolve("routing::Router") - fun router(runtimeConfig: RuntimeConfig) = ServerCargoDependency.smithyHttpServer(runtimeConfig).toType().resolve("routing::Router") - - fun runtimeError(runtimeConfig: RuntimeConfig) = ServerCargoDependency.smithyHttpServer(runtimeConfig).toType().resolve("runtime_error::RuntimeError") - - fun requestRejection(runtimeConfig: RuntimeConfig) = ServerCargoDependency.smithyHttpServer(runtimeConfig).toType().resolve("rejection::RequestRejection") - - fun responseRejection(runtimeConfig: RuntimeConfig) = ServerCargoDependency.smithyHttpServer(runtimeConfig).toType().resolve("rejection::ResponseRejection") - - fun protocol(name: String, path: String, runtimeConfig: RuntimeConfig) = ServerCargoDependency.smithyHttpServer(runtimeConfig).toType().resolve("proto::$path::$name") + fun protocol(name: String, path: String, runtimeConfig: RuntimeConfig) = + ServerCargoDependency.smithyHttpServer(runtimeConfig).toType().resolve("proto::$path::$name") fun protocol(runtimeConfig: RuntimeConfig) = protocol("Protocol", "", runtimeConfig) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecorator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecorator.kt index cebde262b81..98578480dc1 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecorator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/CustomValidationExceptionWithReasonDecorator.kt @@ -35,6 +35,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.StringTraitI import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.isKeyConstrained import software.amazon.smithy.rust.codegen.server.smithy.generators.isValueConstrained +import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage /** @@ -67,11 +68,7 @@ class ValidationExceptionWithReasonConversionGenerator(private val codegenContex override val shapeId: ShapeId = ShapeId.from(codegenContext.settings.codegenConfig.experimentalCustomValidationExceptionWithReasonPleaseDoNotUse) - override fun renderImplFromConstraintViolationForRequestRejection(): Writable = writable { - val codegenScope = arrayOf( - "RequestRejection" to ServerRuntimeType.requestRejection(codegenContext.runtimeConfig), - "From" to RuntimeType.From, - ) + override fun renderImplFromConstraintViolationForRequestRejection(protocol: ServerProtocol): Writable = writable { rustTemplate( """ impl #{From} for #{RequestRejection} { @@ -89,7 +86,8 @@ class ValidationExceptionWithReasonConversionGenerator(private val codegenContex } } """, - *codegenScope, + "RequestRejection" to protocol.requestRejection(codegenContext.runtimeConfig), + "From" to RuntimeType.From, ) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt index 7923f8bb3e4..ebb5c757f22 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/SmithyValidationExceptionDecorator.kt @@ -34,6 +34,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.generators.TraitInfo import software.amazon.smithy.rust.codegen.server.smithy.generators.ValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.isKeyConstrained import software.amazon.smithy.rust.codegen.server.smithy.generators.isValueConstrained +import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol import software.amazon.smithy.rust.codegen.server.smithy.validationErrorMessage /** @@ -66,11 +67,7 @@ class SmithyValidationExceptionConversionGenerator(private val codegenContext: S } override val shapeId: ShapeId = SHAPE_ID - override fun renderImplFromConstraintViolationForRequestRejection(): Writable = writable { - val codegenScope = arrayOf( - "RequestRejection" to ServerRuntimeType.requestRejection(codegenContext.runtimeConfig), - "From" to RuntimeType.From, - ) + override fun renderImplFromConstraintViolationForRequestRejection(protocol: ServerProtocol): Writable = writable { rustTemplate( """ impl #{From} for #{RequestRejection} { @@ -87,7 +84,8 @@ class SmithyValidationExceptionConversionGenerator(private val codegenContext: S } } """, - *codegenScope, + "RequestRejection" to protocol.requestRejection(codegenContext.runtimeConfig), + "From" to RuntimeType.From, ) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt index 2c15fa571fa..a8e458b4d2b 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGenerator.kt @@ -48,6 +48,7 @@ import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType import software.amazon.smithy.rust.codegen.server.smithy.canReachConstrainedShape +import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol import software.amazon.smithy.rust.codegen.server.smithy.hasConstraintTraitOrTargetHasConstraintTrait import software.amazon.smithy.rust.codegen.server.smithy.targetCanReachConstrainedShape import software.amazon.smithy.rust.codegen.server.smithy.traits.ConstraintViolationRustBoxTrait @@ -92,6 +93,7 @@ class ServerBuilderGenerator( val codegenContext: ServerCodegenContext, private val shape: StructureShape, private val customValidationExceptionWithReasonConversionGenerator: ValidationExceptionConversionGenerator, + private val protocol: ServerProtocol, ) { companion object { /** @@ -148,7 +150,7 @@ class ServerBuilderGenerator( ServerBuilderConstraintViolations(codegenContext, shape, takeInUnconstrainedTypes, customValidationExceptionWithReasonConversionGenerator) private val codegenScope = arrayOf( - "RequestRejection" to ServerRuntimeType.requestRejection(runtimeConfig), + "RequestRejection" to protocol.requestRejection(codegenContext.runtimeConfig), "Structure" to structureSymbol, "From" to RuntimeType.From, "TryFrom" to RuntimeType.TryFrom, @@ -222,7 +224,8 @@ class ServerBuilderGenerator( """ #{Converter:W} """, - "Converter" to customValidationExceptionWithReasonConversionGenerator.renderImplFromConstraintViolationForRequestRejection(), + "Converter" to + customValidationExceptionWithReasonConversionGenerator.renderImplFromConstraintViolationForRequestRejection(protocol), ) } diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt index 2ae17d1a80a..c7a87b634a2 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ServerBuilderGeneratorWithoutPublicConstrainedTypes.kt @@ -30,6 +30,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.makeOptional import software.amazon.smithy.rust.codegen.core.smithy.module import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext import software.amazon.smithy.rust.codegen.server.smithy.ServerRuntimeType +import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol import software.amazon.smithy.rust.codegen.server.smithy.withInMemoryInlineModule /** @@ -49,6 +50,7 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( private val codegenContext: ServerCodegenContext, shape: StructureShape, validationExceptionConversionGenerator: ValidationExceptionConversionGenerator, + protocol: ServerProtocol, ) { companion object { /** @@ -85,7 +87,7 @@ class ServerBuilderGeneratorWithoutPublicConstrainedTypes( ServerBuilderConstraintViolations(codegenContext, shape, builderTakesInUnconstrainedTypes = false, validationExceptionConversionGenerator) private val codegenScope = arrayOf( - "RequestRejection" to ServerRuntimeType.requestRejection(codegenContext.runtimeConfig), + "RequestRejection" to protocol.requestRejection(codegenContext.runtimeConfig), "Structure" to structureSymbol, "From" to RuntimeType.From, "TryFrom" to RuntimeType.TryFrom, diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ValidationExceptionConversionGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ValidationExceptionConversionGenerator.kt index 7db44265863..3434afb0b12 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ValidationExceptionConversionGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/ValidationExceptionConversionGenerator.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.model.shapes.StringShape import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol /** * Collection of methods that will be invoked by the respective generators to generate code to convert constraint @@ -26,7 +27,7 @@ interface ValidationExceptionConversionGenerator { * Convert from a top-level operation input's constraint violation into * `aws_smithy_http_server::rejection::RequestRejection`. */ - fun renderImplFromConstraintViolationForRequestRejection(): Writable + fun renderImplFromConstraintViolationForRequestRejection(protocol: ServerProtocol): Writable // Simple shapes. fun stringShapeConstraintViolationImplBlock(stringConstraintsInfo: Collection): Writable diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocol.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocol.kt index ece9b26b422..986f96126d3 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocol.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocol.kt @@ -37,12 +37,10 @@ import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerAwsJson import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerRestJsonSerializerGenerator import software.amazon.smithy.rust.codegen.server.smithy.targetCanReachConstrainedShape -private fun allOperations(codegenContext: CodegenContext): List { - val index = TopDownIndex.of(codegenContext.model) - return index.getContainedOperations(codegenContext.serviceShape).sortedBy { it.id } -} - interface ServerProtocol : Protocol { + /** The path such that `aws_smithy_http_server::proto::$path` points to the protocol's module. */ + val protocolModulePath: String; + /** Returns the Rust marker struct enjoying `OperationShape`. */ fun markerStruct(): RuntimeType @@ -76,6 +74,21 @@ interface ServerProtocol : Protocol { * Returns a boolean indicating whether to perform this check. */ fun serverContentTypeCheckNoModeledInput(): Boolean = false + + /** The protocol-specific `RequestRejection` type. **/ + fun requestRejection(runtimeConfig: RuntimeConfig): RuntimeType = + ServerCargoDependency.smithyHttpServer(runtimeConfig) + .toType().resolve("proto::${protocolModulePath}::rejection::RequestRejection") + + /** The protocol-specific `ResponseRejection` type. **/ + fun responseRejection(runtimeConfig: RuntimeConfig): RuntimeType = + ServerCargoDependency.smithyHttpServer(runtimeConfig) + .toType().resolve("proto::${protocolModulePath}::rejection::ResponseRejection") + + /** The protocol-specific `RuntimeError` type. **/ + fun runtimeError(runtimeConfig: RuntimeConfig): RuntimeType = + ServerCargoDependency.smithyHttpServer(runtimeConfig) + .toType().resolve("proto::${protocolModulePath}::runtime_error::RuntimeError") } class ServerAwsJsonProtocol( @@ -84,6 +97,12 @@ class ServerAwsJsonProtocol( ) : AwsJson(serverCodegenContext, awsJsonVersion), ServerProtocol { private val runtimeConfig = codegenContext.runtimeConfig + override val protocolModulePath: String + get() = when (version) { + is AwsJsonVersion.Json10 -> "aws_json_10" + is AwsJsonVersion.Json11 -> "aws_json_11" + } + override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator { fun returnSymbolToParse(shape: Shape): ReturnSymbolToParse = if (shape.canReachConstrainedShape(codegenContext.model, serverCodegenContext.symbolProvider)) { @@ -107,12 +126,8 @@ class ServerAwsJsonProtocol( override fun markerStruct(): RuntimeType { return when (version) { - is AwsJsonVersion.Json10 -> { - ServerRuntimeType.protocol("AwsJson1_0", "aws_json_10", runtimeConfig) - } - is AwsJsonVersion.Json11 -> { - ServerRuntimeType.protocol("AwsJson1_1", "aws_json_11", runtimeConfig) - } + is AwsJsonVersion.Json10 -> ServerRuntimeType.protocol("AwsJson1_0", protocolModulePath, runtimeConfig) + is AwsJsonVersion.Json11 -> ServerRuntimeType.protocol("AwsJson1_1", protocolModulePath, runtimeConfig) } } @@ -139,6 +154,16 @@ class ServerAwsJsonProtocol( AwsJsonVersion.Json10 -> "new_aws_json_10_router" AwsJsonVersion.Json11 -> "new_aws_json_11_router" } + + override fun requestRejection(runtimeConfig: RuntimeConfig): RuntimeType = + ServerCargoDependency.smithyHttpServer(runtimeConfig) + .toType().resolve("proto::aws_json::rejection::RequestRejection") + override fun responseRejection(runtimeConfig: RuntimeConfig): RuntimeType = + ServerCargoDependency.smithyHttpServer(runtimeConfig) + .toType().resolve("proto::aws_json::rejection::ResponseRejection") + override fun runtimeError(runtimeConfig: RuntimeConfig): RuntimeType = + ServerCargoDependency.smithyHttpServer(runtimeConfig) + .toType().resolve("proto::aws_json::runtime_error::RuntimeError") } private fun restRouterType(runtimeConfig: RuntimeConfig) = @@ -150,6 +175,8 @@ class ServerRestJsonProtocol( ) : RestJson(serverCodegenContext), ServerProtocol { val runtimeConfig = codegenContext.runtimeConfig + override val protocolModulePath: String = "rest_json_1" + override fun structuredDataParser(operationShape: OperationShape): StructuredDataParserGenerator { fun returnSymbolToParse(shape: Shape): ReturnSymbolToParse = if (shape.canReachConstrainedShape(codegenContext.model, codegenContext.symbolProvider)) { @@ -173,7 +200,8 @@ class ServerRestJsonProtocol( override fun structuredDataSerializer(operationShape: OperationShape): StructuredDataSerializerGenerator = ServerRestJsonSerializerGenerator(serverCodegenContext, httpBindingResolver) - override fun markerStruct() = ServerRuntimeType.protocol("RestJson1", "rest_json_1", runtimeConfig) + + override fun markerStruct() = ServerRuntimeType.protocol("RestJson1", protocolModulePath, runtimeConfig) override fun routerType() = restRouterType(runtimeConfig) @@ -196,8 +224,9 @@ class ServerRestXmlProtocol( codegenContext: CodegenContext, ) : RestXml(codegenContext), ServerProtocol { val runtimeConfig = codegenContext.runtimeConfig + override val protocolModulePath = "rest_xml" - override fun markerStruct() = ServerRuntimeType.protocol("RestXml", "rest_xml", runtimeConfig) + override fun markerStruct() = ServerRuntimeType.protocol("RestXml", protocolModulePath, runtimeConfig) override fun routerType() = restRouterType(runtimeConfig) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpBoundProtocolGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpBoundProtocolGenerator.kt index 79340964ac6..134c47d4d38 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpBoundProtocolGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/protocols/ServerHttpBoundProtocolGenerator.kt @@ -131,9 +131,9 @@ class ServerHttpBoundProtocolTraitImplGenerator( "Regex" to RuntimeType.Regex, "SmithyHttp" to RuntimeType.smithyHttp(runtimeConfig), "SmithyHttpServer" to ServerCargoDependency.smithyHttpServer(runtimeConfig).toType(), - "RuntimeError" to ServerRuntimeType.runtimeError(runtimeConfig), - "RequestRejection" to ServerRuntimeType.requestRejection(runtimeConfig), - "ResponseRejection" to ServerRuntimeType.responseRejection(runtimeConfig), + "RuntimeError" to protocol.runtimeError(runtimeConfig), + "RequestRejection" to protocol.requestRejection(runtimeConfig), + "ResponseRejection" to protocol.responseRejection(runtimeConfig), "PinProjectLite" to ServerCargoDependency.PinProjectLite.toType(), "http" to RuntimeType.Http, ) @@ -159,12 +159,11 @@ class ServerHttpBoundProtocolTraitImplGenerator( outputSymbol: Symbol, operationShape: OperationShape, ) { - val operationName = symbolProvider.toSymbol(operationShape).name val verifyAcceptHeader = writable { httpBindingResolver.responseContentType(operationShape)?.also { contentType -> rustTemplate( """ - if ! #{SmithyHttpServer}::protocols::accept_header_classifier(request.headers(), ${contentType.dq()}) { + if !#{SmithyHttpServer}::protocols::accept_header_classifier(request.headers(), ${contentType.dq()}) { return Err(#{RuntimeError}::NotAcceptable) } """, @@ -1149,7 +1148,7 @@ class ServerHttpBoundProtocolTraitImplGenerator( check(binding.location == HttpLocation.PAYLOAD) if (model.expectShape(binding.member.target) is StringShape) { - return ServerRuntimeType.requestRejection(runtimeConfig).toSymbol() + return protocol.requestRejection(runtimeConfig).toSymbol() } return when (codegenContext.protocol) { RestJson1Trait.ID, AwsJson1_0Trait.ID, AwsJson1_1Trait.ID -> { diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt index d7c3c7fb5ce..0991c04e40e 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/testutil/ServerTestHelpers.kt @@ -29,6 +29,7 @@ import software.amazon.smithy.rust.codegen.server.smithy.ServerSymbolProviders import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionConversionGenerator import software.amazon.smithy.rust.codegen.server.smithy.customize.ServerCodegenDecorator import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerBuilderGenerator +import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerProtocolLoader // These are the settings we default to if the user does not override them in their `smithy-build.json`. val ServerTestRustSymbolProviderConfig = RustSymbolProviderConfig( @@ -100,10 +101,7 @@ fun serverTestCodegenContext( settings: ServerRustSettings = serverTestRustSettings(), protocolShapeId: ShapeId? = null, ): ServerCodegenContext { - val service = - serviceShape - ?: model.serviceShapes.firstOrNull() - ?: ServiceShape.builder().version("test").id("test#Service").build() + val service = serviceShape ?: testServiceShapeFor(model) val protocol = protocolShapeId ?: ShapeId.from("test#Protocol") val serverSymbolProviders = ServerSymbolProviders.from( settings, @@ -132,12 +130,28 @@ fun serverTestCodegenContext( /** * In tests, we frequently need to generate a struct, a builder, and an impl block to access said builder. */ -fun StructureShape.serverRenderWithModelBuilder(rustCrate: RustCrate, model: Model, symbolProvider: RustSymbolProvider, writer: RustWriter) { +fun StructureShape.serverRenderWithModelBuilder( + rustCrate: RustCrate, + model: Model, + symbolProvider: RustSymbolProvider, + writer: RustWriter, +) { + // Load the `ServerProtocol`. + val codegenContext = serverTestCodegenContext(model) + val (_, protocolGeneratorFactory) = + ServerProtocolLoader(ServerProtocolLoader.DefaultProtocols).protocolFor(model, codegenContext.serviceShape) + val protocol = protocolGeneratorFactory.buildProtocolGenerator(codegenContext).protocol + StructureGenerator(model, symbolProvider, writer, this, emptyList()).render() val serverCodegenContext = serverTestCodegenContext(model) // Note that this always uses `ServerBuilderGenerator` and _not_ `ServerBuilderGeneratorWithoutPublicConstrainedTypes`, // regardless of the `publicConstrainedTypes` setting. - val modelBuilder = ServerBuilderGenerator(serverCodegenContext, this, SmithyValidationExceptionConversionGenerator(serverCodegenContext)) + val modelBuilder = ServerBuilderGenerator( + serverCodegenContext, + this, + SmithyValidationExceptionConversionGenerator(serverCodegenContext), + protocol + ) modelBuilder.render(rustCrate, writer) writer.implBlock(symbolProvider.toSymbol(this)) { modelBuilder.renderConvenienceMethod(this) diff --git a/rust-runtime/aws-smithy-http-server/src/proto/aws_json/mod.rs b/rust-runtime/aws-smithy-http-server/src/proto/aws_json/mod.rs index a579436e91b..af7b0a809ba 100644 --- a/rust-runtime/aws-smithy-http-server/src/proto/aws_json/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/proto/aws_json/mod.rs @@ -3,4 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ +pub mod rejection; pub mod router; +pub mod runtime_error; diff --git a/rust-runtime/aws-smithy-http-server/src/proto/aws_json/rejection.rs b/rust-runtime/aws-smithy-http-server/src/proto/aws_json/rejection.rs new file mode 100644 index 00000000000..c47057f661f --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/proto/aws_json/rejection.rs @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use strum_macros::Display; + +use crate::rejection::MissingContentTypeReason; + +#[derive(Debug, Display)] +pub enum ResponseRejection { + InvalidHttpStatusCode, + Serialization(crate::Error), + Http(crate::Error), +} + +impl std::error::Error for ResponseRejection {} + +convert_to_response_rejection!(aws_smithy_http::operation::error::SerializationError, Serialization); +convert_to_response_rejection!(http::Error, Http); + +#[derive(Debug, Display)] +pub enum RequestRejection { + HttpBody(crate::Error), + MissingContentType(MissingContentTypeReason), + JsonDeserialize(crate::Error), + ConstraintViolation(String), +} + +impl std::error::Error for RequestRejection {} + +impl From for RequestRejection { + fn from(_err: std::convert::Infallible) -> Self { + match _err {} + } +} + +impl From for RequestRejection { + fn from(e: MissingContentTypeReason) -> Self { + Self::MissingContentType(e) + } +} + +convert_to_request_rejection!(aws_smithy_json::deserialize::error::DeserializeError, JsonDeserialize); + +convert_to_request_rejection!(hyper::Error, HttpBody); + +convert_to_request_rejection!(Box, HttpBody); diff --git a/rust-runtime/aws-smithy-http-server/src/proto/aws_json/runtime_error.rs b/rust-runtime/aws-smithy-http-server/src/proto/aws_json/runtime_error.rs new file mode 100644 index 00000000000..70db8b1e323 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/proto/aws_json/runtime_error.rs @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::proto::aws_json_11::AwsJson1_1; +use crate::response::IntoResponse; +use crate::runtime_error::{InternalFailureException, INVALID_HTTP_RESPONSE_FOR_RUNTIME_ERROR_PANIC_MESSAGE}; +use crate::{extension::RuntimeErrorExtension, proto::aws_json_10::AwsJson1_0}; +use http::StatusCode; + +use super::rejection::{RequestRejection, ResponseRejection}; + +#[derive(Debug)] +pub enum RuntimeError { + Serialization(crate::Error), + InternalFailure(crate::Error), + NotAcceptable, + Validation(String), +} + +impl RuntimeError { + pub fn name(&self) -> &'static str { + match self { + Self::Serialization(_) => "SerializationException", + Self::InternalFailure(_) => "InternalFailureException", + Self::NotAcceptable => "NotAcceptableException", + Self::Validation(_) => "ValidationException", + } + } + + pub fn status_code(&self) -> StatusCode { + match self { + Self::Serialization(_) => StatusCode::BAD_REQUEST, + Self::InternalFailure(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE, + Self::Validation(_) => StatusCode::BAD_REQUEST, + } + } +} + +impl IntoResponse for InternalFailureException { + fn into_response(self) -> http::Response { + IntoResponse::::into_response(RuntimeError::InternalFailure(crate::Error::new(String::new()))) + } +} + +impl IntoResponse for InternalFailureException { + fn into_response(self) -> http::Response { + IntoResponse::::into_response(RuntimeError::InternalFailure(crate::Error::new(String::new()))) + } +} + +impl IntoResponse for RuntimeError { + fn into_response(self) -> http::Response { + let res = http::Response::builder() + .status(self.status_code()) + .header("Content-Type", "application/x-amz-json-1.0") + .extension(RuntimeErrorExtension::new(self.name().to_string())); + + let body = match self { + RuntimeError::Validation(reason) => crate::body::to_boxed(reason), + // See https://awslabs.github.io/smithy/2.0/aws/protocols/aws-json-1_0-protocol.html#empty-body-serialization + _ => crate::body::to_boxed("{}"), + }; + + res.body(body) + .expect(INVALID_HTTP_RESPONSE_FOR_RUNTIME_ERROR_PANIC_MESSAGE) + } +} + +impl IntoResponse for RuntimeError { + fn into_response(self) -> http::Response { + let res = http::Response::builder() + .status(self.status_code()) + .header("Content-Type", "application/x-amz-json-1.1") + .extension(RuntimeErrorExtension::new(self.name().to_string())); + + let body = match self { + RuntimeError::Validation(reason) => crate::body::to_boxed(reason), + _ => crate::body::to_boxed(""), + }; + + res.body(body) + .expect(INVALID_HTTP_RESPONSE_FOR_RUNTIME_ERROR_PANIC_MESSAGE) + } +} + +impl From for RuntimeError { + fn from(err: ResponseRejection) -> Self { + Self::Serialization(crate::Error::new(err)) + } +} + +impl From for RuntimeError { + fn from(err: RequestRejection) -> Self { + match err { + RequestRejection::ConstraintViolation(reason) => Self::Validation(reason), + _ => Self::Serialization(crate::Error::new(err)), + } + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/proto/aws_json_10/router.rs b/rust-runtime/aws-smithy-http-server/src/proto/aws_json_10/router.rs index 31d5ce8a9e1..50e73e21981 100644 --- a/rust-runtime/aws-smithy-http-server/src/proto/aws_json_10/router.rs +++ b/rust-runtime/aws-smithy-http-server/src/proto/aws_json_10/router.rs @@ -24,7 +24,7 @@ impl IntoResponse for Error { UNKNOWN_OPERATION_EXCEPTION.to_string(), )) .body(empty()) - .expect("invalid HTTP response for AWS JSON routing error; please file a bug report under https://github.com/awslabs/smithy-rs/issues"), + .expect("invalid HTTP response for AWS JSON 1.0 routing error; please file a bug report under https://github.com/awslabs/smithy-rs/issues"), } } } diff --git a/rust-runtime/aws-smithy-http-server/src/proto/aws_json_11/router.rs b/rust-runtime/aws-smithy-http-server/src/proto/aws_json_11/router.rs index 18f3b4b3293..c7d1176e988 100644 --- a/rust-runtime/aws-smithy-http-server/src/proto/aws_json_11/router.rs +++ b/rust-runtime/aws-smithy-http-server/src/proto/aws_json_11/router.rs @@ -24,7 +24,7 @@ impl IntoResponse for Error { UNKNOWN_OPERATION_EXCEPTION.to_string(), )) .body(empty()) - .expect("invalid HTTP response for AWS JSON routing error; please file a bug report under https://github.com/awslabs/smithy-rs/issues"), + .expect("invalid HTTP response for AWS JSON 1.1 routing error; please file a bug report under https://github.com/awslabs/smithy-rs/issues"), } } } diff --git a/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/mod.rs b/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/mod.rs index db7c64a84b3..567a26f5bd4 100644 --- a/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/mod.rs @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +pub mod rejection; pub mod router; +pub mod runtime_error; /// [AWS REST JSON 1.0 Protocol](https://awslabs.github.io/smithy/2.0/aws/protocols/aws-restjson1-protocol.html). pub struct RestJson1; diff --git a/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/rejection.rs b/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/rejection.rs new file mode 100644 index 00000000000..7a670ae1d1b --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/rejection.rs @@ -0,0 +1,221 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Rejection types. +//! +//! This module contains types that are commonly used as the `E` error type in functions that +//! handle requests and responses that return `Result` throughout the framework. These +//! include functions to deserialize incoming requests and serialize outgoing responses. +//! +//! All types end with `Rejection`. There are two types: +//! +//! 1. [`RequestRejection`]s are used when the framework fails to deserialize the request into the +//! corresponding operation input. +//! 2. [`ResponseRejection`]s are used when the framework fails to serialize the operation +//! output into a response. +//! +//! They are called _rejection_ types and not _error_ types to signal that the input was _rejected_ +//! (as opposed to it causing a recoverable error that would need to be handled, or an +//! unrecoverable error). For example, a [`RequestRejection`] simply means that the request was +//! rejected; there isn't really anything wrong with the service itself that the service +//! implementer would need to handle. +//! +//! Rejection types are an _internal_ detail about the framework: they can be added, removed, and +//! modified at any time without causing breaking changes. They are not surfaced to clients or the +//! service implementer in any way (including this documentation): indeed, they can't be converted +//! into responses. They serve as a mechanism to keep track of all the possible errors that can +//! occur when processing a request or a response, in far more detail than what AWS protocols need +//! to. This is why they are so granular: other (possibly protocol-specific) error types (like +//! [`crate::proto::rest_json_1::runtime_error::RuntimeError`]) can "group" them when exposing +//! errors to clients while the framework does not need to sacrifice fidelity in private error +//! handling routines, and future-proofing itself at the same time (for example, we might want to +//! record metrics about rejection types). +//! +//! Rejection types implement [`std::error::Error`], and some take in type-erased boxed errors +//! (`crate::Error`) to represent their underlying causes, so they can be composed with other types +//! that take in (possibly type-erased) [`std::error::Error`]s, like +//! [`crate::proto::rest_json_1::runtime_error::RuntimeError`], thus allowing us to represent the +//! full error chain. +//! +//! This module hosts rejection types _specific_ to the [`crate::proto::rest_json_1`] protocol, but +//! the paragraphs above apply to _all_ protocol-specific rejection types. +//! +//! Similarly, rejection type variants are exhaustively documented solely in this module if they have +//! direct counterparts in other protocols. This is to avoid documentation getting out of date. +//! +//! Consult `crate::proto::$protocolName::rejection` for rejection types for other protocols. + +use strum_macros::Display; + +use crate::rejection::MissingContentTypeReason; + +/// Errors that can occur when serializing the operation output provided by the service implementer +/// into an HTTP response. +#[derive(Debug, Display)] +pub enum ResponseRejection { + /// Used when the service implementer provides an integer outside the 100-999 range for a + /// member targeted by `httpResponseCode`. + /// See . + InvalidHttpStatusCode, + + /// Used when an invalid HTTP header value (a value that cannot be parsed as an + /// `[http::header::HeaderValue]`) is provided for a shape member bound to an HTTP header with + /// `httpHeader` or `httpPrefixHeaders`. + /// Used when failing to serialize an `httpPayload`-bound struct into an HTTP response body. + Build(crate::Error), + + /// Used when failing to serialize a struct into a `String` for the HTTP response body (for + /// example, converting a struct into a JSON-encoded `String`). + Serialization(crate::Error), + + /// Used when consuming an [`http::response::Builder`] into the constructed [`http::Response`] + /// when calling [`http::response::Builder::body`]. + /// This error can happen if an invalid HTTP header value (a value that cannot be parsed as an + /// `[http::header::HeaderValue]`) is used for the protocol-specific response `Content-Type` + /// header, or for additional protocol-specific headers (like `X-Amzn-Errortype` to signal + /// errors in RestJson1). + Http(crate::Error), +} + +impl std::error::Error for ResponseRejection {} + +convert_to_response_rejection!(aws_smithy_http::operation::error::BuildError, Build); +convert_to_response_rejection!(aws_smithy_http::operation::error::SerializationError, Serialization); +convert_to_response_rejection!(http::Error, Http); + +/// Errors that can occur when deserializing an HTTP request into an _operation input_, the input +/// that is passed as the first argument to operation handlers. +/// +/// This type allows us to easily keep track of all the possible errors that can occur in the +/// lifecycle of an incoming HTTP request. +/// +/// Many inner code-generated and runtime deserialization functions use this as their error type, +/// when they can only instantiate a subset of the variants (most likely a single one). This is a +/// deliberate design choice to keep code generation simple. After all, this type is an inner +/// detail of the framework the service implementer does not interact with. +/// +/// If a variant takes in a value, it represents the underlying cause of the error. This inner +/// value should be of the type-erased boxed error type `[crate::Error]`. In practice, some of the +/// variants that take in a value are only instantiated with errors of a single type in the +/// generated code. For example, `UriPatternMismatch` is only instantiated with an error coming +/// from a `nom` parser, `nom::Err>`. This is reflected in the converters +/// below that convert from one of these very specific error types into one of the variants. For +/// example, the `RequestRejection` implements `From` to construct the `HttpBody` +/// variant. This is a deliberate design choice to make the code simpler and less prone to changes. +/// +/// The variants are _roughly_ sorted in the order in which the HTTP request is processed. +#[derive(Debug, Display)] +pub enum RequestRejection { + /// Used when failing to convert non-streaming requests into a byte slab with + /// `hyper::body::to_bytes`. + HttpBody(crate::Error), + + /// Used when checking the `Content-Type` header. + MissingContentType(MissingContentTypeReason), + + /// Used when failing to deserialize the HTTP body's bytes into a JSON document conforming to + /// the modeled input it should represent. + JsonDeserialize(crate::Error), + + /// Used when failing to parse HTTP headers that are bound to input members with the `httpHeader` + /// or the `httpPrefixHeaders` traits. + HeaderParse(crate::Error), + + /// Used when the URI pattern has a literal after the greedy label, and it is not found in the + /// request's URL. + UriPatternGreedyLabelPostfixNotFound, + /// Used when the `nom` parser's input does not match the URI pattern. + UriPatternMismatch(crate::Error), + + /// Used when percent-decoding URL query string. + /// Used when percent-decoding URI path label. + InvalidUtf8(crate::Error), + + /// Used when failing to deserialize strings from a URL query string and from URI path labels + /// into an [`aws_smithy_types::DateTime`]. + DateTimeParse(crate::Error), + + /// Used when failing to deserialize strings from a URL query string and from URI path labels + /// into "primitive" types. + PrimitiveParse(crate::Error), + + // The following three variants are used when failing to deserialize strings from a URL query + // string and URI path labels into "primitive" types. + // TODO(https://github.com/awslabs/smithy-rs/issues/1232): They should be removed and + // conflated into the `PrimitiveParse` variant above after this issue is resolved. + IntParse(crate::Error), + FloatParse(crate::Error), + BoolParse(crate::Error), + + /// Used when consuming the input struct builder, and constraint violations occur. + // Unlike the rejections above, this does not take in `crate::Error`, since it is constructed + // directly in the code-generated SDK instead of in this crate. + ConstraintViolation(String), +} + +impl std::error::Error for RequestRejection {} + +// Consider a conversion between `T` and `U` followed by a bubbling up of the conversion error +// through `Result<_, RequestRejection>`. This [`From`] implementation accomodates the special case +// where `T` and `U` are equal, in such cases `T`/`U` a enjoy `TryFrom` with +// `Err = std::convert::Infallible`. +// +// Note that when `!` stabilizes `std::convert::Infallible` will become an alias for `!` and there +// will be a blanket `impl From for T`. This will remove the need for this implementation. +// +// More details on this can be found in the following links: +// - https://doc.rust-lang.org/std/primitive.never.html +// - https://doc.rust-lang.org/std/convert/enum.Infallible.html#future-compatibility +impl From for RequestRejection { + fn from(_err: std::convert::Infallible) -> Self { + // We opt for this `match` here rather than [`unreachable`] to assure the reader that this + // code path is dead. + match _err {} + } +} + +impl From for RequestRejection { + fn from(e: MissingContentTypeReason) -> Self { + Self::MissingContentType(e) + } +} + +// These converters are solely to make code-generation simpler. They convert from a specific error +// type (from a runtime/third-party crate or the standard library) into a variant of the +// [`crate::rejection::RequestRejection`] enum holding the type-erased boxed [`crate::Error`] +// type. Generated functions that use [crate::rejection::RequestRejection] can thus use `?` to +// bubble up instead of having to sprinkle things like [`Result::map_err`] everywhere. + +convert_to_request_rejection!(aws_smithy_json::deserialize::error::DeserializeError, JsonDeserialize); +convert_to_request_rejection!(aws_smithy_http::header::ParseError, HeaderParse); +convert_to_request_rejection!(aws_smithy_types::date_time::DateTimeParseError, DateTimeParse); +convert_to_request_rejection!(aws_smithy_types::primitive::PrimitiveParseError, PrimitiveParse); +convert_to_request_rejection!(std::str::ParseBoolError, BoolParse); +convert_to_request_rejection!(std::num::ParseFloatError, FloatParse); +convert_to_request_rejection!(std::num::ParseIntError, IntParse); +convert_to_request_rejection!(serde_urlencoded::de::Error, InvalidUtf8); + +impl From>> for RequestRejection { + fn from(err: nom::Err>) -> Self { + Self::UriPatternMismatch(crate::Error::new(err.to_owned())) + } +} + +// Used when calling +// [`percent_encoding::percent_decode_str`](https://docs.rs/percent-encoding/latest/percent_encoding/fn.percent_decode_str.html) +// and bubbling up. +// This can happen when the percent-encoded data in e.g. a query string decodes to bytes that are +// not a well-formed UTF-8 string. +convert_to_request_rejection!(std::str::Utf8Error, InvalidUtf8); + +// `[crate::body::Body]` is `[hyper::Body]`, whose associated `Error` type is `[hyper::Error]`. We +// need this converter for when we convert the body into bytes in the framework, since protocol +// tests use `[crate::body::Body]` as their body type when constructing requests (and almost +// everyone will run a Hyper-based server in their services). +convert_to_request_rejection!(hyper::Error, HttpBody); + +// Useful in general, but it also required in order to accept Lambda HTTP requests using +// `Router` since `lambda_http::Error` is a type alias for `Box`. +convert_to_request_rejection!(Box, HttpBody); diff --git a/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/router.rs b/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/router.rs index 189658d317a..3a2a5111b6c 100644 --- a/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/router.rs +++ b/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/router.rs @@ -24,7 +24,7 @@ impl IntoResponse for Error { UNKNOWN_OPERATION_EXCEPTION.to_string(), )) .body(crate::body::to_boxed("{}")) - .expect("invalid HTTP response for REST JSON routing error; please file a bug report under https://github.com/awslabs/smithy-rs/issues"), + .expect("invalid HTTP response for REST JSON 1 routing error; please file a bug report under https://github.com/awslabs/smithy-rs/issues"), Error::MethodNotAllowed => method_disallowed(), } } diff --git a/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/runtime_error.rs b/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/runtime_error.rs new file mode 100644 index 00000000000..fdf55e623a9 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/proto/rest_json_1/runtime_error.rs @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Runtime error type. +//! +//! This module contains the [`RuntimeError`] type. +//! +//! As opposed to rejection types (see [`crate::proto::rest_json_1::rejection`]), which are an internal detail about +//! the framework, `RuntimeError` is surfaced to clients in HTTP responses: indeed, it implements +//! [`RuntimeError::into_response`]. Rejections can be "grouped" and converted into a +//! specific `RuntimeError` kind: for example, all request rejections due to serialization issues +//! can be conflated under the [`RuntimeError::Serialization`] enum variant. +//! +//! The HTTP response representation of the specific `RuntimeError` is protocol-specific: for +//! example, the runtime error in the [`crate::proto::rest_json_1`] protocol sets the `X-Amzn-Errortype` header. +//! +//! Generated code works always works with [`crate::rejection`] types when deserializing requests +//! and serializing response. Just before a response needs to be sent, the generated code looks up +//! and converts into the corresponding `RuntimeError`, and then it uses the its +//! [`RuntimeError::into_response`] method to render and send a response. +//! +//! This module hosts the `RuntimeError` type _specific_ to the [`crate::proto::rest_json_1`] protocol, but +//! the paragraphs above apply to _all_ protocol-specific rejection types. +//! +//! Similarly, `RuntimeError` variants are exhaustively documented solely in this module if they have +//! direct counterparts in other protocols. This is to avoid documentation getting out of date. +//! +//! Consult `crate::proto::$protocolName::runtime_error` for the `RuntimeError` type for other protocols. + +use super::rejection::RequestRejection; +use super::rejection::ResponseRejection; +use super::RestJson1; +use crate::extension::RuntimeErrorExtension; +use crate::response::IntoResponse; +use crate::runtime_error::InternalFailureException; +use crate::runtime_error::INVALID_HTTP_RESPONSE_FOR_RUNTIME_ERROR_PANIC_MESSAGE; +use http::StatusCode; + +#[derive(Debug)] +pub enum RuntimeError { + /// Request failed to deserialize or response failed to serialize. + Serialization(crate::Error), + /// As of writing, this variant can only occur upon failure to extract an + /// [`crate::extension::Extension`] from the request. + InternalFailure(crate::Error), + /// Request contained an `Accept` header with a MIME type, and the server cannot return a response + /// body adhering to that MIME type. + NotAcceptable, + /// The request does not contain the expected `Content-Type` header value. + UnsupportedMediaType, + /// Operation input contains data that does not adhere to the modeled [constraint traits]. + /// [constraint traits]: + Validation(String), +} + +impl RuntimeError { + /// String representation of the `RuntimeError` kind. + /// Used as the value passed to construct an [`crate::extension::RuntimeErrorExtension`]. + /// Used as the value of the `X-Amzn-Errortype` header. + pub fn name(&self) -> &'static str { + match self { + Self::Serialization(_) => "SerializationException", + Self::InternalFailure(_) => "InternalFailureException", + Self::NotAcceptable => "NotAcceptableException", + Self::UnsupportedMediaType => "UnsupportedMediaTypeException", + Self::Validation(_) => "ValidationException", + } + } + + pub fn status_code(&self) -> StatusCode { + match self { + Self::Serialization(_) => StatusCode::BAD_REQUEST, + Self::InternalFailure(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE, + Self::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE, + Self::Validation(_) => StatusCode::BAD_REQUEST, + } + } +} + +impl IntoResponse for InternalFailureException { + fn into_response(self) -> http::Response { + IntoResponse::::into_response(RuntimeError::InternalFailure(crate::Error::new(String::new()))) + } +} + +impl IntoResponse for RuntimeError { + fn into_response(self) -> http::Response { + let res = http::Response::builder() + .status(self.status_code()) + .header("Content-Type", "application/json") + .header("X-Amzn-Errortype", self.name()) + .extension(RuntimeErrorExtension::new(self.name().to_string())); + + let body = match self { + RuntimeError::Validation(reason) => crate::body::to_boxed(reason), + _ => crate::body::to_boxed("{}"), + }; + + res.body(body) + .expect(INVALID_HTTP_RESPONSE_FOR_RUNTIME_ERROR_PANIC_MESSAGE) + } +} + +impl From for RuntimeError { + fn from(err: ResponseRejection) -> Self { + Self::Serialization(crate::Error::new(err)) + } +} + +impl From for RuntimeError { + fn from(err: RequestRejection) -> Self { + match err { + RequestRejection::MissingContentType(_reason) => Self::UnsupportedMediaType, + RequestRejection::ConstraintViolation(reason) => Self::Validation(reason), + _ => Self::Serialization(crate::Error::new(err)), + } + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/mod.rs b/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/mod.rs index ba6ed3d0192..1924afbbbb6 100644 --- a/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/mod.rs @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +pub mod rejection; pub mod router; +pub mod runtime_error; /// [AWS REST XML Protocol](https://awslabs.github.io/smithy/2.0/aws/protocols/aws-restxml-protocol.html). pub struct RestXml; diff --git a/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/rejection.rs b/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/rejection.rs new file mode 100644 index 00000000000..e3e54baa927 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/rejection.rs @@ -0,0 +1,99 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! This module hosts _exactly_ the same as [`crate::proto::rest_json_1::rejection`], expect that +//! [`crate::proto::rest_json_1::rejection::RequestRejection::JsonDeserialize`] is swapped for +//! [`RequestRejection::XmlDeserialize`]. + +use strum_macros::Display; + +#[derive(Debug, Display)] +pub enum ResponseRejection { + InvalidHttpStatusCode, + Build(crate::Error), + Serialization(crate::Error), + Http(crate::Error), +} + +impl std::error::Error for ResponseRejection {} + +convert_to_response_rejection!(aws_smithy_http::operation::error::BuildError, Build); +convert_to_response_rejection!(aws_smithy_http::operation::error::SerializationError, Serialization); +convert_to_response_rejection!(http::Error, Http); + +#[derive(Debug, Display)] +pub enum RequestRejection { + HttpBody(crate::Error), + + MissingContentType(MissingContentTypeReason), + + /// Used when failing to deserialize the HTTP body's bytes into a XML conforming to the modeled + /// input it should represent. + XmlDeserialize(crate::Error), + + HeaderParse(crate::Error), + + UriPatternGreedyLabelPostfixNotFound, + UriPatternMismatch(crate::Error), + + InvalidUtf8(crate::Error), + + DateTimeParse(crate::Error), + + PrimitiveParse(crate::Error), + + IntParse(crate::Error), + FloatParse(crate::Error), + BoolParse(crate::Error), + + ConstraintViolation(String), +} + +#[derive(Debug, Display)] +pub enum MissingContentTypeReason { + HeadersTakenByAnotherExtractor, + NoContentTypeHeader, + ToStrError(http::header::ToStrError), + MimeParseError(mime::FromStrError), + UnexpectedMimeType { + expected_mime: Option, + found_mime: Option, + }, +} + +impl std::error::Error for RequestRejection {} + +impl From for RequestRejection { + fn from(_err: std::convert::Infallible) -> Self { + match _err {} + } +} + +impl From for RequestRejection { + fn from(e: MissingContentTypeReason) -> Self { + Self::MissingContentType(e) + } +} + +convert_to_request_rejection!(aws_smithy_xml::decode::XmlDecodeError, XmlDeserialize); +convert_to_request_rejection!(aws_smithy_http::header::ParseError, HeaderParse); +convert_to_request_rejection!(aws_smithy_types::date_time::DateTimeParseError, DateTimeParse); +convert_to_request_rejection!(aws_smithy_types::primitive::PrimitiveParseError, PrimitiveParse); +convert_to_request_rejection!(std::str::ParseBoolError, BoolParse); +convert_to_request_rejection!(std::num::ParseFloatError, FloatParse); +convert_to_request_rejection!(std::num::ParseIntError, IntParse); +convert_to_request_rejection!(serde_urlencoded::de::Error, InvalidUtf8); + +impl From>> for RequestRejection { + fn from(err: nom::Err>) -> Self { + Self::UriPatternMismatch(crate::Error::new(err.to_owned())) + } +} + +convert_to_request_rejection!(std::str::Utf8Error, InvalidUtf8); + +convert_to_request_rejection!(hyper::Error, HttpBody); + +convert_to_request_rejection!(Box, HttpBody); diff --git a/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/runtime_error.rs b/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/runtime_error.rs new file mode 100644 index 00000000000..3083df61db4 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/proto/rest_xml/runtime_error.rs @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::proto::rest_xml::RestXml; +use crate::response::IntoResponse; +use crate::runtime_error::InternalFailureException; +use crate::{extension::RuntimeErrorExtension, runtime_error::INVALID_HTTP_RESPONSE_FOR_RUNTIME_ERROR_PANIC_MESSAGE}; +use http::StatusCode; + +use super::rejection::{RequestRejection, ResponseRejection}; + +#[derive(Debug)] +pub enum RuntimeError { + Serialization(crate::Error), + InternalFailure(crate::Error), + NotAcceptable, + UnsupportedMediaType, + Validation(String), +} + +impl RuntimeError { + pub fn name(&self) -> &'static str { + match self { + Self::Serialization(_) => "SerializationException", + Self::InternalFailure(_) => "InternalFailureException", + Self::NotAcceptable => "NotAcceptableException", + Self::UnsupportedMediaType => "UnsupportedMediaTypeException", + Self::Validation(_) => "ValidationException", + } + } + + pub fn status_code(&self) -> StatusCode { + match self { + Self::Serialization(_) => StatusCode::BAD_REQUEST, + Self::InternalFailure(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE, + Self::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE, + Self::Validation(_) => StatusCode::BAD_REQUEST, + } + } +} + +impl IntoResponse for InternalFailureException { + fn into_response(self) -> http::Response { + IntoResponse::::into_response(RuntimeError::InternalFailure(crate::Error::new(String::new()))) + } +} + +impl IntoResponse for RuntimeError { + fn into_response(self) -> http::Response { + let res = http::Response::builder() + .status(self.status_code()) + .header("Content-Type", "application/xml") + .extension(RuntimeErrorExtension::new(self.name().to_string())); + + let body = match self { + _ => crate::body::to_boxed("{}"), + }; + + res.body(body) + .expect(INVALID_HTTP_RESPONSE_FOR_RUNTIME_ERROR_PANIC_MESSAGE) + } +} + +impl From for RuntimeError { + fn from(err: ResponseRejection) -> Self { + Self::Serialization(crate::Error::new(err)) + } +} + +impl From for RuntimeError { + fn from(err: RequestRejection) -> Self { + match err { + RequestRejection::MissingContentType(_reason) => Self::UnsupportedMediaType, + RequestRejection::ConstraintViolation(reason) => Self::Validation(reason), + _ => Self::Serialization(crate::Error::new(err)), + } + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/protocols.rs b/rust-runtime/aws-smithy-http-server/src/protocols.rs index 98d0223971a..a2428a768b8 100644 --- a/rust-runtime/aws-smithy-http-server/src/protocols.rs +++ b/rust-runtime/aws-smithy-http-server/src/protocols.rs @@ -33,7 +33,7 @@ fn parse_content_type(headers: &HeaderMap) -> Result() // `expected_content_type` comes from the codegen. - .expect("BUG: MIME parsing failed, expected_content_type is not valid. Please file a bug report under https://github.com/awslabs/smithy-rs/issues"); + .expect("BUG: MIME parsing failed, `expected_content_type` is not valid. Please file a bug report under https://github.com/awslabs/smithy-rs/issues"); if expected_content_type != found_mime { return Err(MissingContentTypeReason::UnexpectedMimeType { expected_mime: Some(expected_mime), @@ -57,7 +57,7 @@ pub fn content_type_header_classifier( }); } } else { - // Content-type header and no modeled input (mismatch) + // `content-type` header and no modeled input (mismatch). return Err(MissingContentTypeReason::UnexpectedMimeType { expected_mime: None, found_mime: Some(found_mime), @@ -84,10 +84,10 @@ pub fn accept_header_classifier(headers: &HeaderMap, content_type: &'static str) .ok() .into_iter() /* - * turn a header value of: "type0/subtype0, type1/subtype1, ..." + * Turn a header value of: "type0/subtype0, type1/subtype1, ..." * into: ["type0/subtype0", "type1/subtype1", ...] * and remove the optional "; q=x" parameters - * NOTE: the unwrap() is safe, because it takes the first element (if there's nothing to split, returns the string) + * NOTE: the `unwrap`() is safe, because it takes the first element (if there's nothing to split, returns the string) */ .flat_map(|s| s.split(',').map(|typ| typ.split(';').next().unwrap().trim())) }) diff --git a/rust-runtime/aws-smithy-http-server/src/rejection.rs b/rust-runtime/aws-smithy-http-server/src/rejection.rs index f01344f7657..209a6721f38 100644 --- a/rust-runtime/aws-smithy-http-server/src/rejection.rs +++ b/rust-runtime/aws-smithy-http-server/src/rejection.rs @@ -3,158 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! Rejection types. -//! -//! This module contains types that are commonly used as the `E` error type in functions that -//! handle requests and responses that return `Result` throughout the framework. These -//! include functions to deserialize incoming requests and serialize outgoing responses. -//! -//! All types end with `Rejection`. There are two types: -//! -//! 1. [`RequestRejection`]s are used when the framework fails to deserialize the request into the -//! corresponding operation input. -//! 2. [`ResponseRejection`]s are used when the framework fails to serialize the operation -//! output into a response. -//! -//! They are called _rejection_ types and not _error_ types to signal that the input was _rejected_ -//! (as opposed to it causing a recoverable error that would need to be handled, or an -//! unrecoverable error). For example, a [`RequestRejection`] simply means that the request was -//! rejected; there isn't really anything wrong with the service itself that the service -//! implementer would need to handle. -//! -//! Rejection types are an _internal_ detail about the framework: they can be added, removed, and -//! modified at any time without causing breaking changes. They are not surfaced to clients or the -//! service implementer in any way (including this documentation): indeed, they can't be converted -//! into responses. They serve as a mechanism to keep track of all the possible errors that can -//! occur when processing a request or a response, in far more detail than what AWS protocols need -//! to. This is why they are so granular: other (possibly protocol-specific) error types (like -//! [`crate::runtime_error::RuntimeError`]) can "group" them when exposing errors to -//! clients while the framework does not need to sacrifice fidelity in private error handling -//! routines, and future-proofing itself at the same time (for example, we might want to record -//! metrics about rejection types). -//! -//! Rejection types implement [`std::error::Error`], and some take in type-erased boxed errors -//! (`crate::Error`) to represent their underlying causes, so they can be composed with other types -//! that take in (possibly type-erased) [`std::error::Error`]s, like -//! [`crate::runtime_error::RuntimeError`], thus allowing us to represent the full -//! error chain. - use strum_macros::Display; use crate::response::IntoResponse; -/// Errors that can occur when serializing the operation output provided by the service implementer -/// into an HTTP response. -#[derive(Debug, Display)] -pub enum ResponseRejection { - /// Used when the service implementer provides an integer outside the 100-999 range for a - /// member targeted by `httpResponseCode`. - InvalidHttpStatusCode, - - /// Used when an invalid HTTP header value (a value that cannot be parsed as an - /// `[http::header::HeaderValue]`) is provided for a shape member bound to an HTTP header with - /// `httpHeader` or `httpPrefixHeaders`. - /// Used when failing to serialize an `httpPayload`-bound struct into an HTTP response body. - Build(crate::Error), - - /// Used when failing to serialize a struct into a `String` for the HTTP response body (for - /// example, converting a struct into a JSON-encoded `String`). - Serialization(crate::Error), - - /// Used when consuming an [`http::response::Builder`] into the constructed [`http::Response`] - /// when calling [`http::response::Builder::body`]. - /// This error can happen if an invalid HTTP header value (a value that cannot be parsed as an - /// `[http::header::HeaderValue]`) is used for the protocol-specific response `Content-Type` - /// header, or for additional protocol-specific headers (like `X-Amzn-Errortype` to signal - /// errors in RestJson1). - Http(crate::Error), -} - -impl std::error::Error for ResponseRejection {} - -convert_to_response_rejection!(aws_smithy_http::operation::error::BuildError, Build); -convert_to_response_rejection!(aws_smithy_http::operation::error::SerializationError, Serialization); -convert_to_response_rejection!(http::Error, Http); - -/// Errors that can occur when deserializing an HTTP request into an _operation input_, the input -/// that is passed as the first argument to operation handlers. -/// -/// This type allows us to easily keep track of all the possible errors that can occur in the -/// lifecycle of an incoming HTTP request. -/// -/// Many inner code-generated and runtime deserialization functions use this as their error type, when they can -/// only instantiate a subset of the variants (most likely a single one). For example, the -/// functions that check the `Content-Type` header in `[crate::protocols]` can only return three of -/// the variants: `MissingJsonContentType`, `MissingXmlContentType`, and `MimeParse`. -/// This is a deliberate design choice to keep code generation simple. After all, this type is an -/// inner detail of the framework the service implementer does not interact with. It allows us to -/// easily keep track of all the possible errors that can occur in the lifecycle of an incoming -/// HTTP request. -/// -/// If a variant takes in a value, it represents the underlying cause of the error. This inner -/// value should be of the type-erased boxed error type `[crate::Error]`. In practice, some of the -/// variants that take in a value are only instantiated with errors of a single type in the -/// generated code. For example, `UriPatternMismatch` is only instantiated with an error coming -/// from a `nom` parser, `nom::Err>`. This is reflected in the converters -/// below that convert from one of these very specific error types into one of the variants. For -/// example, the `RequestRejection` implements `From` to construct the `HttpBody` -/// variant. This is a deliberate design choice to make the code simpler and less prone to changes. -/// -// The variants are _roughly_ sorted in the order in which the HTTP request is processed. -#[derive(Debug, Display)] -pub enum RequestRejection { - /// Used when failing to convert non-streaming requests into a byte slab with - /// `hyper::body::to_bytes`. - HttpBody(crate::Error), - - /// Used when checking the `Content-Type` header. - MissingContentType(MissingContentTypeReason), - - /// Used when failing to deserialize the HTTP body's bytes into a JSON document conforming to - /// the modeled input it should represent. - JsonDeserialize(crate::Error), - /// Used when failing to deserialize the HTTP body's bytes into a XML conforming to the modeled - /// input it should represent. - XmlDeserialize(crate::Error), - - /// Used when failing to parse HTTP headers that are bound to input members with the `httpHeader` - /// or the `httpPrefixHeaders` traits. - HeaderParse(crate::Error), - - /// Used when the URI pattern has a literal after the greedy label, and it is not found in the - /// request's URL. - UriPatternGreedyLabelPostfixNotFound, - /// Used when the `nom` parser's input does not match the URI pattern. - UriPatternMismatch(crate::Error), - - /// Used when percent-decoding URL query string. - /// Used when percent-decoding URI path label. - InvalidUtf8(crate::Error), - - /// Used when failing to deserialize strings from a URL query string and from URI path labels - /// into an [`aws_smithy_types::DateTime`]. - DateTimeParse(crate::Error), - - /// Used when failing to deserialize strings from a URL query string and from URI path labels - /// into "primitive" types. - PrimitiveParse(crate::Error), - - // The following three variants are used when failing to deserialize strings from a URL query - // string and URI path labels into "primitive" types. - // TODO(https://github.com/awslabs/smithy-rs/issues/1232): They should be removed and - // conflated into the `PrimitiveParse` variant above after this issue is resolved. - IntParse(crate::Error), - FloatParse(crate::Error), - BoolParse(crate::Error), - - /// Used when consuming the input struct builder, and constraint violations occur. - // Unlike the rejections above, this does not take in `crate::Error`, since it is constructed - // directly in the code-generated SDK instead of in this crate. - // TODO(https://github.com/awslabs/smithy-rs/issues/1703): this will hold a type that can be - // rendered into a protocol-specific response later on. - ConstraintViolation(String), -} - +// This is used across different protocol-specific `rejection` modules. #[derive(Debug, Display)] pub enum MissingContentTypeReason { HeadersTakenByAnotherExtractor, @@ -167,72 +20,6 @@ pub enum MissingContentTypeReason { }, } -impl std::error::Error for RequestRejection {} - -// Consider a conversion between `T` and `U` followed by a bubbling up of the conversion error -// through `Result<_, RequestRejection>`. This [`From`] implementation accomodates the special case -// where `T` and `U` are equal, in such cases `T`/`U` a enjoy `TryFrom` with -// `Err = std::convert::Infallible`. -// -// Note that when `!` stabilizes `std::convert::Infallible` will become an alias for `!` and there -// will be a blanket `impl From for T`. This will remove the need for this implementation. -// -// More details on this can be found in the following links: -// - https://doc.rust-lang.org/std/primitive.never.html -// - https://doc.rust-lang.org/std/convert/enum.Infallible.html#future-compatibility -impl From for RequestRejection { - fn from(_err: std::convert::Infallible) -> Self { - // We opt for this `match` here rather than [`unreachable`] to assure the reader that this - // code path is dead. - match _err {} - } -} - -impl From for RequestRejection { - fn from(e: MissingContentTypeReason) -> Self { - Self::MissingContentType(e) - } -} - -// These converters are solely to make code-generation simpler. They convert from a specific error -// type (from a runtime/third-party crate or the standard library) into a variant of the -// [`crate::rejection::RequestRejection`] enum holding the type-erased boxed [`crate::Error`] -// type. Generated functions that use [crate::rejection::RequestRejection] can thus use `?` to -// bubble up instead of having to sprinkle things like [`Result::map_err`] everywhere. - -convert_to_request_rejection!(aws_smithy_json::deserialize::error::DeserializeError, JsonDeserialize); -convert_to_request_rejection!(aws_smithy_xml::decode::XmlDecodeError, XmlDeserialize); -convert_to_request_rejection!(aws_smithy_http::header::ParseError, HeaderParse); -convert_to_request_rejection!(aws_smithy_types::date_time::DateTimeParseError, DateTimeParse); -convert_to_request_rejection!(aws_smithy_types::primitive::PrimitiveParseError, PrimitiveParse); -convert_to_request_rejection!(std::str::ParseBoolError, BoolParse); -convert_to_request_rejection!(std::num::ParseFloatError, FloatParse); -convert_to_request_rejection!(std::num::ParseIntError, IntParse); -convert_to_request_rejection!(serde_urlencoded::de::Error, InvalidUtf8); - -impl From>> for RequestRejection { - fn from(err: nom::Err>) -> Self { - Self::UriPatternMismatch(crate::Error::new(err.to_owned())) - } -} - -// Used when calling -// [`percent_encoding::percent_decode_str`](https://docs.rs/percent-encoding/latest/percent_encoding/fn.percent_decode_str.html) -// and bubbling up. -// This can happen when the percent-encoded data in e.g. a query string decodes to bytes that are -// not a well-formed UTF-8 string. -convert_to_request_rejection!(std::str::Utf8Error, InvalidUtf8); - -// `[crate::body::Body]` is `[hyper::Body]`, whose associated `Error` type is `[hyper::Error]`. We -// need this converter for when we convert the body into bytes in the framework, since protocol -// tests use `[crate::body::Body]` as their body type when constructing requests (and almost -// everyone will run a Hyper-based server in their services). -convert_to_request_rejection!(hyper::Error, HttpBody); - -// Useful in general, but it also required in order to accept Lambda HTTP requests using -// `Router` since `lambda_http::Error` is a type alias for `Box`. -convert_to_request_rejection!(Box, HttpBody); - pub mod any_rejections { //! This module hosts enums, up to size 8, which implement [`IntoResponse`] when their variants implement //! [`IntoResponse`]. diff --git a/rust-runtime/aws-smithy-http-server/src/routing/request_spec.rs b/rust-runtime/aws-smithy-http-server/src/routing/request_spec.rs index 84f431c24d5..b4d9f3887c5 100644 --- a/rust-runtime/aws-smithy-http-server/src/routing/request_spec.rs +++ b/rust-runtime/aws-smithy-http-server/src/routing/request_spec.rs @@ -154,7 +154,7 @@ impl RequestSpec { /// So this ranking of routes implements some basic pattern conflict disambiguation with some /// common sense. It's also the same behavior that [the TypeScript sSDK is implementing]. /// - /// TODO(https://github.com/awslabs/smithy/issues/1029#issuecomment-1002683552): Once Smithy + // TODO(https://github.com/awslabs/smithy/issues/1029#issuecomment-1002683552): Once Smithy /// updates the spec to define the behavior, update our implementation. /// /// [the TypeScript sSDK is implementing]: https://github.com/awslabs/smithy-typescript/blob/d263078b81485a6a2013d243639c0c680343ff47/smithy-typescript-ssdk-libs/server-common/src/httpbinding/mux.ts#L59. diff --git a/rust-runtime/aws-smithy-http-server/src/runtime_error.rs b/rust-runtime/aws-smithy-http-server/src/runtime_error.rs index 9fc1b8e1a77..97dca6ad195 100644 --- a/rust-runtime/aws-smithy-http-server/src/runtime_error.rs +++ b/rust-runtime/aws-smithy-http-server/src/runtime_error.rs @@ -3,189 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! Runtime error type. -//! -//! This module contains [`RuntimeError`] type. -//! -//! As opposed to rejection types (see [`crate::rejection`]), which are an internal detail about -//! the framework, `RuntimeError` is surfaced to clients in HTTP responses: indeed, it implements -//! [`RuntimeError::into_response`]. Rejections can be "grouped" and converted into a -//! specific `RuntimeError` kind: for example, all request rejections due to serialization issues -//! can be conflated under the [`RuntimeError::Serialization`] enum variant. -//! -//! The HTTP response representation of the specific `RuntimeError` can be protocol-specific: for -//! example, the runtime error in the RestJson1 protocol sets the `X-Amzn-Errortype` header. -//! -//! Generated code works always works with [`crate::rejection`] types when deserializing requests -//! and serializing response. Just before a response needs to be sent, the generated code looks up -//! and converts into the corresponding `RuntimeError`, and then it uses the its -//! [`RuntimeError::into_response`] method to render and send a response. - -use crate::extension::RuntimeErrorExtension; -use crate::proto::aws_json_10::AwsJson1_0; -use crate::proto::aws_json_11::AwsJson1_1; -use crate::proto::rest_json_1::RestJson1; -use crate::proto::rest_xml::RestXml; -use crate::response::IntoResponse; -use http::StatusCode; - -#[derive(Debug)] -pub enum RuntimeError { - /// Request failed to deserialize or response failed to serialize. - Serialization(crate::Error), - /// As of writing, this variant can only occur upon failure to extract an - /// [`crate::extension::Extension`] from the request. - InternalFailure(crate::Error), - // TODO(https://github.com/awslabs/smithy-rs/issues/1663) - NotAcceptable, - UnsupportedMediaType, - - // TODO(https://github.com/awslabs/smithy-rs/issues/1703): this will hold a type that can be - // rendered into a protocol-specific response later on. - Validation(String), -} - -/// String representation of the runtime error type. -/// Used as the value of the `X-Amzn-Errortype` header in RestJson1. -/// Used as the value passed to construct an [`crate::extension::RuntimeErrorExtension`]. -impl RuntimeError { - pub fn name(&self) -> &'static str { - match self { - Self::Serialization(_) => "SerializationException", - Self::InternalFailure(_) => "InternalFailureException", - Self::NotAcceptable => "NotAcceptableException", - Self::UnsupportedMediaType => "UnsupportedMediaTypeException", - Self::Validation(_) => "ValidationException", - } - } - - pub fn status_code(&self) -> StatusCode { - match self { - Self::Serialization(_) => StatusCode::BAD_REQUEST, - Self::InternalFailure(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::NotAcceptable => StatusCode::NOT_ACCEPTABLE, - Self::UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE, - Self::Validation(_) => StatusCode::BAD_REQUEST, - } - } -} - +/// A _protocol-agnostic_ type representing an internal framework error. As of writing, this can only +/// occur upon failure to extract an [`crate::extension::Extension`] from the request. +/// This type is converted into protocol-specific error variants. For example, in the +/// [`crate::proto::rest_json_1`] protocol, it is converted to the +/// [`crate::proto::rest_json_1::runtime_error::RuntimeError::InternalFailure`] variant. pub struct InternalFailureException; -impl IntoResponse for InternalFailureException { - fn into_response(self) -> http::Response { - IntoResponse::::into_response(RuntimeError::InternalFailure(crate::Error::new(String::new()))) - } -} - -impl IntoResponse for InternalFailureException { - fn into_response(self) -> http::Response { - IntoResponse::::into_response(RuntimeError::InternalFailure(crate::Error::new(String::new()))) - } -} - -impl IntoResponse for InternalFailureException { - fn into_response(self) -> http::Response { - IntoResponse::::into_response(RuntimeError::InternalFailure(crate::Error::new(String::new()))) - } -} - -impl IntoResponse for InternalFailureException { - fn into_response(self) -> http::Response { - IntoResponse::::into_response(RuntimeError::InternalFailure(crate::Error::new(String::new()))) - } -} - -impl IntoResponse for RuntimeError { - fn into_response(self) -> http::Response { - let res = http::Response::builder() - .status(self.status_code()) - .header("Content-Type", "application/json") - .header("X-Amzn-Errortype", self.name()) - .extension(RuntimeErrorExtension::new(self.name().to_string())); - - let body = match self { - RuntimeError::Validation(reason) => crate::body::to_boxed(reason), - _ => crate::body::to_boxed("{}"), - }; - - res - .body(body) - .expect("invalid HTTP response for `RuntimeError`; please file a bug report under https://github.com/awslabs/smithy-rs/issues") - } -} - -impl IntoResponse for RuntimeError { - fn into_response(self) -> http::Response { - let res = http::Response::builder() - .status(self.status_code()) - .header("Content-Type", "application/xml") - .extension(RuntimeErrorExtension::new(self.name().to_string())); - - let body = match self { - // TODO(https://github.com/awslabs/smithy/issues/1446) The Smithy spec does not yet - // define constraint violation HTTP body responses for RestXml. - RuntimeError::Validation(_reason) => todo!("https://github.com/awslabs/smithy/issues/1446"), - // See https://awslabs.github.io/smithy/1.0/spec/aws/aws-json-1_1-protocol.html#empty-body-serialization - _ => crate::body::to_boxed("{}"), - }; - - res - .body(body) - .expect("invalid HTTP response for `RuntimeError`; please file a bug report under https://github.com/awslabs/smithy-rs/issues") - } -} - -impl IntoResponse for RuntimeError { - fn into_response(self) -> http::Response { - let res = http::Response::builder() - .status(self.status_code()) - .header("Content-Type", "application/x-amz-json-1.0") - .extension(RuntimeErrorExtension::new(self.name().to_string())); - - let body = match self { - RuntimeError::Validation(reason) => crate::body::to_boxed(reason), - // See https://awslabs.github.io/smithy/2.0/aws/protocols/aws-json-1_0-protocol.html#empty-body-serialization - _ => crate::body::to_boxed("{}"), - }; - - res - .body(body) - .expect("invalid HTTP response for `RuntimeError`; please file a bug report under https://github.com/awslabs/smithy-rs/issues") - } -} - -impl IntoResponse for RuntimeError { - fn into_response(self) -> http::Response { - let res = http::Response::builder() - .status(self.status_code()) - .header("Content-Type", "application/x-amz-json-1.1") - .extension(RuntimeErrorExtension::new(self.name().to_string())); - - let body = match self { - RuntimeError::Validation(reason) => crate::body::to_boxed(reason), - // https://awslabs.github.io/smithy/2.0/aws/protocols/aws-json-1_1-protocol.html#empty-body-serialization - _ => crate::body::to_boxed(""), - }; - - res - .body(body) - .expect("invalid HTTP response for `RuntimeError`; please file a bug report under https://github.com/awslabs/smithy-rs/issues") - } -} - -impl From for RuntimeError { - fn from(err: crate::rejection::ResponseRejection) -> Self { - Self::Serialization(crate::Error::new(err)) - } -} - -impl From for RuntimeError { - fn from(err: crate::rejection::RequestRejection) -> Self { - match err { - crate::rejection::RequestRejection::MissingContentType(_reason) => Self::UnsupportedMediaType, - crate::rejection::RequestRejection::ConstraintViolation(reason) => Self::Validation(reason), - _ => Self::Serialization(crate::Error::new(err)), - } - } -} +pub const INVALID_HTTP_RESPONSE_FOR_RUNTIME_ERROR_PANIC_MESSAGE: &str = "invalid HTTP response for `RuntimeError`; please file a bug report under https://github.com/awslabs/smithy-rs/issues";