Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changelog/1761080224.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
applies_to:
- server
authors:
- rcoh
references: ["smithy-rs#4356"]
breaking: true
new_feature: true
bug_fix: false
---
Parse EventStream signed-frames for servers marked with `@sigv4`.

This is a breaking change, because events from SigV4 services are wrapped in a SignedEvent frame.
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ operation MyOperation {
- **`codegen-core/common-test-models/constraints.smithy`** - Constraint validation tests with restJson1
- **`codegen-client-test/model/main.smithy`** - awsJson1_1 protocol tests

### httpQueryParams Bug Investigation
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't think you meant to leave info for a specific bug in the AGENTS file?


When investigating the `@httpQueryParams` bug (where query parameters weren't appearing in requests), the issue was in `RequestBindingGenerator.kt` line 173. The bug occurred when:

1. An operation had ONLY `@httpQueryParams` (no regular `@httpQuery` parameters)
2. The condition `if (dynamicParams.isEmpty() && literalParams.isEmpty() && mapParams.isEmpty())` would skip generating the `uri_query` function

The fix was to ensure `mapParams.isEmpty()` was included in the condition check. The current implementation correctly generates query parameters for `@httpQueryParams` even when no other query parameters exist.

**Testing httpQueryParams**: Create operations with only `@httpQueryParams` to ensure they generate proper query strings in requests.

## rustTemplate Formatting

**CRITICAL**: Because `#` is the formatting character in `rustTemplate`, Rust attributes must be escaped:

❌ Wrong: `#[derive(Debug)]`
✅ Correct: `##[derive(Debug)]`

This applies to ALL Rust attributes: `##[non_exhaustive]`, `##[derive(...)]`, `##[cfg(...)]`, etc.

## preludeScope: Rust Prelude Types

**Always use `preludeScope` for Rust prelude types:**
Expand Down
2 changes: 2 additions & 0 deletions codegen-core/common-test-models/rpcv2Cbor-extras.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ use smithy.framework#ValidationException
use smithy.protocols#rpcv2Cbor
use smithy.test#httpResponseTests
use smithy.test#httpMalformedRequestTests
use aws.auth#sigv4

@rpcv2Cbor
@sigv4(name: "rpcv2-cbor")
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there an existing test service without @sigv4 to verify the code generator works for unsigned services? If not, perhaps instead of adding this annotation directly to the .smithy file, we could add it as part of a separate test to ensure both signed and unsigned services are covered?

service RpcV2CborService {
operations: [
SimpleStructOperation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,14 @@ fun Symbol.makeMaybeConstrained(): Symbol =
* WARNING: This function does not update any symbol references (e.g., `symbol.addReference()`) on the
* returned symbol. You will have to add those yourself if your logic relies on them.
**/
fun Symbol.mapRustType(f: (RustType) -> RustType): Symbol {
fun Symbol.mapRustType(
vararg dependencies: RuntimeType,
f: (RustType) -> RustType,
): Symbol {
val newType = f(this.rustType())
return Symbol.builder().rustType(newType)
val builder = this.toBuilder()
dependencies.forEach { builder.addReference(it.toSymbol()) }
return builder.rustType(newType)
.name(newType.name)
.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ sealed class HttpBindingSection(name: String) : Section(name) {

data class AfterDeserializingIntoADateTimeOfHttpHeaders(val memberShape: MemberShape) :
HttpBindingSection("AfterDeserializingIntoADateTimeOfHttpHeaders")

data class BeforeCreatingEventStreamReceiver(
val operationShape: OperationShape,
val unionShape: UnionShape,
val unmarshallerVariableName: String,
) : HttpBindingSection("BeforeCreatingEventStreamReceiver")
}

typealias HttpBindingCustomization = NamedCustomization<HttpBindingSection>
Expand Down Expand Up @@ -272,11 +278,27 @@ class HttpBindingGenerator(
rustTemplate(
"""
let unmarshaller = #{unmarshallerConstructorFn}();
""",
"unmarshallerConstructorFn" to unmarshallerConstructorFn,
)

// Allow customizations to wrap the unmarshaller
for (customization in customizations) {
customization.section(
HttpBindingSection.BeforeCreatingEventStreamReceiver(
operationShape,
targetShape,
"unmarshaller",
),
)(this)
}

rustTemplate(
"""
let body = std::mem::replace(body, #{SdkBody}::taken());
Ok(#{receiver:W})
""",
"SdkBody" to RuntimeType.sdkBody(runtimeConfig),
"unmarshallerConstructorFn" to unmarshallerConstructorFn,
"receiver" to
writable {
if (codegenTarget == CodegenTarget.SERVER) {
Expand Down
18 changes: 9 additions & 9 deletions codegen-server-test/integration-tests/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,15 @@ async fn streaming_operation_handler(
state.lock().unwrap().streaming_operation.num_calls += 1;
let ev = input.events.recv().await;

if let Ok(Some(event)) = &ev {
if let Ok(Some(signed_event)) = &ev {
// Extract the actual event from the SignedEvent wrapper
let actual_event = &signed_event.message;
state
.lock()
.unwrap()
.streaming_operation
.events
.push(event.clone());
.push(actual_event.clone());
}

Ok(output::StreamingOperationOutput::builder()
Expand All @@ -174,13 +176,15 @@ async fn streaming_operation_with_initial_data_handler(

let ev = input.events.recv().await;

if let Ok(Some(event)) = &ev {
if let Ok(Some(signed_event)) = &ev {
// Extract the actual event from the SignedEvent wrapper
let actual_event = &signed_event.message;
state
.lock()
.unwrap()
.streaming_operation_with_initial_data
.events
.push(event.clone());
.push(actual_event.clone());
}

Ok(output::StreamingOperationWithInitialDataOutput::builder()
Expand Down Expand Up @@ -229,7 +233,7 @@ async fn streaming_operation_with_optional_data_handler(
.unwrap()
.streaming_operation_with_optional_data
.events
.push(event.clone());
.push(event.message.clone());
}

Ok(output::StreamingOperationWithOptionalDataOutput::builder()
Expand Down Expand Up @@ -348,6 +352,39 @@ fn build_event(event_type: &str) -> Message {
Message::new_from_parts(headers, empty_cbor)
}

fn build_sigv4_signed_event(event_type: &str) -> Message {
use aws_smithy_eventstream::frame::write_message_to;
use std::time::{SystemTime, UNIX_EPOCH};

// Build the inner event message
let inner_event = build_event(event_type);

// Serialize the inner message to bytes
let mut inner_bytes = Vec::new();
write_message_to(&inner_event, &mut inner_bytes).unwrap();

// Create the SigV4 envelope with signature headers
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();

let headers = vec![
Header::new(
":chunk-signature",
HeaderValue::ByteArray(Bytes::from(
"example298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
)),
),
Header::new(
":date",
HeaderValue::Timestamp(aws_smithy_types::DateTime::from_secs(timestamp as i64)),
),
];

Message::new_from_parts(headers, Bytes::from(inner_bytes))
}

fn get_event_type(msg: &Message) -> &str {
msg.headers()
.iter()
Expand Down Expand Up @@ -439,6 +476,24 @@ async fn test_streaming_operation_with_initial_data_missing() {
);
}

/// Test that the server can handle SigV4 signed event stream messages.
/// The client wraps the actual event in a SigV4 envelope with signature headers.
#[tokio::test]
async fn test_sigv4_signed_event_stream() {
let mut harness = TestHarness::new("StreamingOperation").await;

// Send a SigV4 signed event - the inner message is wrapped in an envelope
let signed_event = build_sigv4_signed_event("A");
harness.client.send(signed_event).await.unwrap();

let resp = harness.expect_message().await;
assert_eq!(get_event_type(&resp), "A");
assert_eq!(
harness.server.streaming_operation_events(),
vec![Events::A(Event {})]
);
}

/// Test that when alwaysSendEventStreamInitialResponse is disabled, no initial-response is sent
#[tokio::test]
async fn test_server_no_initial_response_when_disabled() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class TsServerCodegenVisitor(
ServerProtocolLoader(
codegenDecorator.protocols(
service.id,
ServerProtocolLoader.DefaultProtocols,
ServerProtocolLoader.defaultProtocols(),
),
)
.protocolFor(context.model, service)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.StreamingShapeSymbolProvi
import software.amazon.smithy.rust.codegen.core.smithy.SymbolVisitor
import software.amazon.smithy.rust.codegen.server.smithy.customizations.CustomValidationExceptionWithReasonDecorator
import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations
import software.amazon.smithy.rust.codegen.server.smithy.customizations.SigV4EventStreamDecorator
import software.amazon.smithy.rust.codegen.server.smithy.customizations.SmithyValidationExceptionDecorator
import software.amazon.smithy.rust.codegen.server.smithy.customizations.UserProvidedValidationExceptionDecorator
import software.amazon.smithy.rust.codegen.server.smithy.customize.CombinedServerCodegenDecorator
Expand Down Expand Up @@ -54,6 +55,7 @@ class RustServerCodegenPlugin : ServerDecoratableBuildPlugin() {
UserProvidedValidationExceptionDecorator(),
SmithyValidationExceptionDecorator(),
CustomValidationExceptionWithReasonDecorator(),
SigV4EventStreamDecorator(),
*decorator,
)
logger.info("Loaded plugin to generate pure Rust bindings for the server SDK")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,7 @@ open class ServerCodegenVisitor(

val baseModel = baselineTransform(context.model)
val service = settings.getService(baseModel)
val (protocolShape, protocolGeneratorFactory) =
ServerProtocolLoader(
codegenDecorator.protocols(
service.id,
ServerProtocolLoader.DefaultProtocols,
),
)
.protocolFor(context.model, service)
this.protocolGeneratorFactory = protocolGeneratorFactory

model = codegenDecorator.transformModel(service, baseModel, settings)

val serverSymbolProviders =
ServerSymbolProviders.from(
settings,
Expand All @@ -146,7 +135,19 @@ open class ServerCodegenVisitor(
codegenDecorator,
RustServerCodegenPlugin::baseSymbolProvider,
)

val (protocolShape, protocolGeneratorFactory) =
ServerProtocolLoader(
codegenDecorator.protocols(
service.id,
ServerProtocolLoader.defaultProtocols { it ->
codegenDecorator.httpCustomizations(
serverSymbolProviders.symbolProvider,
it,
)
},
),
)
.protocolFor(context.model, service)
codegenContext =
ServerCodegenContext(
model,
Expand All @@ -160,6 +161,7 @@ open class ServerCodegenVisitor(
serverSymbolProviders.constraintViolationSymbolProvider,
serverSymbolProviders.pubCrateConstrainedShapeSymbolProvider,
)
this.protocolGeneratorFactory = protocolGeneratorFactory

// We can use a not-null assertion because [CombinedServerCodegenDecorator] returns a not null value.
validationExceptionConversionGenerator = codegenDecorator.validationExceptionConversion(codegenContext)!!
Expand Down
Loading
Loading