From b5ef801e68f973d617a51ed910006b93125ab3ec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 07:11:52 +0000 Subject: [PATCH 1/2] PLAN: record Phase 6 follow-ups (consumer OS matrix, incremental dev flows) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WjaNFwBZxoHdqagvq8ycQf --- docs/PLAN.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/PLAN.md b/docs/PLAN.md index 1c67edac..0b4b4887 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -497,6 +497,14 @@ without reading generator internals or touching Gradle. test) — if the tutorial breaks, CI fails. - Bazel rules tested against the out-of-tree consumer module on current Bazel 9 releases (tracked via bazelisk pinning). +- **Follow-up**: run the consumer acceptance job on the full OS matrix (Linux/macOS/Windows), + not just Linux. +- **Follow-up — incremental development flows**: support and document the model-evolution loop + after initial integration: change the model (add an operation or member, tighten a + constraint), rebuild, and let the handler-interface compile errors guide the update; + regenerate vendored CLI output safely; keep hand-written handlers and tests working across + regenerations. Exercise the flow in CI (scripted evolve-and-rebuild against the consumer + example). **Docs:** the docs site is the deliverable. From 8907aba81e3ccf5d17ec2a05af01582815fdcf7e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 07:39:45 +0000 Subject: [PATCH 2/2] Phase 7a: client retries with backoff + gzip @requestCompression Client robustness and the first Phase 7 hardening slice: - runtime //runtime:client gains RetryPolicy (full-jitter exponential backoff, injectable sleep/jitter) and SendWithRetries: transport failures flagged retryable and transient statuses (429/500/502/503/504) are retried up to max_attempts (default 3). Generated clients send through it; generated test suites pin max_attempts = 1 to stay wire-exact, and a weather e2e test drives the generated client through transient transport failures. - runtime //runtime:compression (zlib via BCR): GzipCompress / GzipDecompress with a decompression-bomb output cap and trailing-garbage rejection. Generated clients gzip request bodies of @requestCompression operations at the Smithy-default 10 KiB threshold (ClientConfig.request_min_compression_size_bytes), appending to any member-bound Content-Encoding header; generated server routes transparently gunzip such requests and reject malformed gzip with a 400 serialization error. The suite's two client compression cases are un-excluded and green; ~1,175 conformance cases still pass. - consumer CI job now runs the quick-start module on the full OS matrix (linux/macos/windows); bazel/defs.bzl adds //runtime:compression to the generated-library deps. - docs: new production guide (timeouts, retries, compression), runtime overview + server guide + README status updates. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WjaNFwBZxoHdqagvq8ycQf --- .github/workflows/ci.yml | 11 +- MODULE.bazel | 1 + MODULE.bazel.lock | 3 +- README.md | 2 +- bazel/defs.bzl | 1 + codegen/smithy-cpp-codegen/build.gradle.kts | 5 +- .../smithycpp/codegen/BuildFileGenerator.java | 9 +- .../io/smithycpp/codegen/ClientGenerator.java | 2 +- .../smithycpp/codegen/DirectedCppCodegen.java | 10 +- .../codegen/IntegrationTestGenerator.java | 2 + .../io/smithycpp/codegen/ProtocolSupport.java | 56 +++++++++ .../codegen/ProtocolTestGenerator.java | 2 + .../smithycpp/codegen/RestJson1Protocol.java | 15 ++- .../smithycpp/codegen/Rpcv2CborProtocol.java | 16 ++- .../smithycpp/codegen/SmokeTestGenerator.java | 1 + .../codegen/protocol-test-exclusions.txt | 11 +- docs/production-guide.md | 95 +++++++++++++++ docs/runtime.md | 3 +- docs/server-guide.md | 6 +- examples/bazel-consumer/MODULE.bazel.lock | 3 +- examples/cafe/generated/src/client.cc | 2 +- .../cafe/generated/tests/integration_test.cc | 3 + examples/cafe/generated/tests/smoke_test.cc | 1 + .../roundtrip/rest/generated/src/client.cc | 2 +- .../rest/generated/tests/integration_test.cc | 3 + .../rest/generated/tests/smoke_test.cc | 1 + .../roundtrip/rpc/generated/src/client.cc | 2 +- .../rpc/generated/tests/integration_test.cc | 2 + .../rpc/generated/tests/smoke_test.cc | 1 + examples/weather/BUILD.bazel | 2 + examples/weather/generated/src/client.cc | 2 +- .../generated/tests/integration_test.cc | 5 + .../weather/generated/tests/smoke_test.cc | 1 + examples/weather/generated_server_e2e_test.cc | 33 +++++ .../generated/BUILD.bazel | 2 + .../generated/src/client.cc | 2 +- .../generated/tests/smoke_test.cc | 1 + .../restjson1/generated/BUILD.bazel | 2 + .../restjson1/generated/src/client.cc | 12 +- .../restjson1/generated/src/server.cc | 12 +- .../generated/tests/request_tests.cc | 36 +++++- .../generated/tests/response_tests.cc | 1 + .../generated/tests/server_request_tests.cc | 4 +- .../generated/tests/server_response_tests.cc | 36 ++++++ .../restjson1/generated/tests/smoke_test.cc | 1 + .../rpcv2cbor/generated/BUILD.bazel | 2 + .../rpcv2cbor/generated/src/client.cc | 2 +- .../generated/tests/request_tests.cc | 1 + .../generated/tests/response_tests.cc | 1 + .../generated/tests/server_response_tests.cc | 9 ++ .../rpcv2cbor/generated/tests/smoke_test.cc | 1 + runtime/BUILD.bazel | 42 ++++++- runtime/include/smithy/client/config.h | 9 ++ runtime/include/smithy/client/retry.h | 44 +++++++ runtime/include/smithy/compression/gzip.h | 22 ++++ runtime/src/client/retry.cc | 53 ++++++++ runtime/src/compression/gzip.cc | 72 +++++++++++ runtime/tests/client/retry_test.cc | 113 ++++++++++++++++++ runtime/tests/compression/gzip_test.cc | 47 ++++++++ 59 files changed, 803 insertions(+), 38 deletions(-) create mode 100644 docs/production-guide.md create mode 100644 runtime/include/smithy/client/retry.h create mode 100644 runtime/include/smithy/compression/gzip.h create mode 100644 runtime/src/client/retry.cc create mode 100644 runtime/src/compression/gzip.cc create mode 100644 runtime/tests/client/retry_test.cc create mode 100644 runtime/tests/compression/gzip_test.cc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5968b4e8..6cd5201f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,13 +48,18 @@ jobs: run: bazelisk test //... --config=ci --config=asan --config=ubsan consumer: - name: bazel consumer (quick start) - runs-on: ubuntu-24.04 + name: bazel consumer (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-14, windows-2022] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 # The quick-start acceptance test (docs/quickstart.md): a standalone # out-of-tree Bazel module consumes smithy_cpp via the rules in - # bazel/defs.bzl — generation runs inside the consumer's build graph. + # bazel/defs.bzl — generation runs inside the consumer's build graph, + # on every OS the runtime itself supports. - name: bazel test (out-of-tree module) working-directory: examples/bazel-consumer run: bazelisk test //... --verbose_failures diff --git a/MODULE.bazel b/MODULE.bazel index 23868073..459c6923 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -10,6 +10,7 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2") bazel_dep(name = "boost.asio", version = "1.87.0.bcr.1") bazel_dep(name = "boost.beast", version = "1.87.0") bazel_dep(name = "nlohmann_json", version = "3.12.0.bcr.1") +bazel_dep(name = "zlib", version = "1.3.1.bcr.8") # boost.container 1.87's registry overlay calls native.cc_test without a # load(), the one stale file in the boost 1.87 graph (audited); patch it diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index fdb38ea9..6cd6f098 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -314,7 +314,8 @@ "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.8/MODULE.bazel": "772c674bb78a0342b8caf32ab5c25085c493ca4ff08398208dcbe4375fe9f776", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.8/source.json": "cf377d76800dfc3d3b71e9dd4a8c53a62837cbce37cc4f25e6207b15fc1e8f2b", "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" }, "selectedYankedVersions": {}, diff --git a/README.md b/README.md index bde858e7..fca3fc4d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ See [`docs/PLAN.md`](docs/PLAN.md) for the full phased plan and | 4 | Server generation (restJson1 + rpcv2Cbor) | ✅ Done — handlers, routing, serde, all HTTP bindings incl. `@httpPayload`/`@httpPrefixHeaders`, constraint validation, parser strictness, content negotiation; ~1,175 official conformance cases green ([docs/server-guide.md](docs/server-guide.md)) | | 5 | Generated-client ↔ generated-server integration harness | ✅ Done — every fixture ships a generated integration suite: seeded random round-trips over loopback and real sockets, per-error mapping, unknown-member tolerance, mutation-checked ([docs/design/integration-testing.md](docs/design/integration-testing.md)) | | 6 | Bazel rules, CLI, packaging (BCR + Maven Central), docs site | 🔨 In progress — `smithy_cpp_{types,client,server}_library` rules run the generator hermetically inside the build graph, out-of-tree consumer module tested in CI, CLI via `bazel run //codegen:generator` ([docs/quickstart.md](docs/quickstart.md)); BCR/Maven publishing deferred until production validation; docs site pending | -| 7 | Hardening, fuzzing, v0.1.0 | Not started | +| 7 | Hardening, fuzzing, v0.1.0 | 🔨 In progress — retries with full-jitter exponential backoff, gzip `@requestCompression` (client + server), consumer CI across linux/macos/windows ([docs/production-guide.md](docs/production-guide.md)) | | 8 | Bidirectional streaming (event streams, WebSockets) | Not started | ## Building diff --git a/bazel/defs.bzl b/bazel/defs.bzl index cacb593e..0f5c2f2d 100644 --- a/bazel/defs.bzl +++ b/bazel/defs.bzl @@ -23,6 +23,7 @@ _GENERATOR = Label("//codegen:generator") _RUNTIME_DEPS = [ Label("//runtime:cbor"), Label("//runtime:client"), + Label("//runtime:compression"), Label("//runtime:core"), Label("//runtime:http"), Label("//runtime:json"), diff --git a/codegen/smithy-cpp-codegen/build.gradle.kts b/codegen/smithy-cpp-codegen/build.gradle.kts index 570de40f..a53df015 100644 --- a/codegen/smithy-cpp-codegen/build.gradle.kts +++ b/codegen/smithy-cpp-codegen/build.gradle.kts @@ -122,9 +122,8 @@ val generateRpcv2CborProtocolTests = registerProtocolTestTask( ) // The constraint-validation suite: httpMalformedRequestTests only (no -// httpRequestTests/httpResponseTests), so malformed generation is enabled for -// this module. The main suites' malformed tests (parser strictness) are a -// Phase 4d follow-up. +// httpRequestTests/httpResponseTests). The main suites above also run their +// malformed tests (parser strictness, since Phase 4d). val generateRestJson1ValidationProtocolTests = registerProtocolTestTask( "generateRestJson1ValidationProtocolTests", "aws.protocoltests.restjson.validation#RestJsonValidation", diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/BuildFileGenerator.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/BuildFileGenerator.java index 4f3009ff..0c56503e 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/BuildFileGenerator.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/BuildFileGenerator.java @@ -14,7 +14,8 @@ static void run( ProtocolGenerator protocol, boolean hasClient, boolean hasSerde, - boolean hasServer) { + boolean hasServer, + boolean hasCompression) { CppSettings settings = context.settings(); StringBuilder out = new StringBuilder(); out.append( @@ -59,6 +60,9 @@ static void run( deps.add("\"" + pkg + ":client\""); deps.add("\"" + pkg + ":core\""); deps.add("\"" + pkg + ":http\""); + if (hasCompression) { + deps.add("\"" + pkg + ":compression\""); + } for (String dep : protocol.runtimeDeps()) { deps.add("\"" + pkg + dep + "\""); } @@ -83,6 +87,9 @@ static void run( serverDeps.add("\"" + pkg + ":core\""); serverDeps.add("\"" + pkg + ":http\""); serverDeps.add("\"" + pkg + ":server\""); + if (hasCompression) { + serverDeps.add("\"" + pkg + ":compression\""); + } for (String dep : protocol.runtimeDeps()) { serverDeps.add("\"" + pkg + dep + "\""); } diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ClientGenerator.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ClientGenerator.java index e1147ff9..aacc9725 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ClientGenerator.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ClientGenerator.java @@ -158,7 +158,7 @@ private void writeSource(CppWriter w) { w.openBlock("if (!request.body.empty()) {"); w.write("request.headers.Set(\"content-length\", std::to_string(request.body.size()));"); w.closeBlock("}"); - w.write("return transport_->Send(request);"); + w.write("return smithy::SendWithRetries(*transport_, request, config_.retry);"); w.closeBlock("}"); w.write(""); diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/DirectedCppCodegen.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/DirectedCppCodegen.java index 4980425c..39c8c235 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/DirectedCppCodegen.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/DirectedCppCodegen.java @@ -110,7 +110,15 @@ public void generateService(GenerateServiceDirective di } } if (directive.settings().emitBuildFile()) { - BuildFileGenerator.run(directive.context(), protocol, hasClient, hasSerde, hasServer); + boolean hasCompression = + protocol != null + && directive.context().model().getOperationShapes().stream() + .anyMatch( + op -> + op.hasTrait( + software.amazon.smithy.model.traits.RequestCompressionTrait.class)); + BuildFileGenerator.run( + directive.context(), protocol, hasClient, hasSerde, hasServer, hasCompression); } } diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/IntegrationTestGenerator.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/IntegrationTestGenerator.java index dc1c1229..6eac8739 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/IntegrationTestGenerator.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/IntegrationTestGenerator.java @@ -182,6 +182,7 @@ private void writeFixture(CppWriter w) { w.write("handler_ = std::make_shared();"); w.write("server_ = std::make_unique<$LServer>(handler_);", name); w.write("smithy::ClientConfig config;"); + w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries"); w.openBlock("if (GetParam() == TransportKind::kLoopback) {"); w.write("auto loopback = std::make_shared();"); w.write("ASSERT_TRUE(loopback->Start(server_->Handler()).ok());"); @@ -305,6 +306,7 @@ private void writeUnknownFieldTest(CppWriter w, OperationShape operation) { "auto transport = std::make_shared(loopback, " + "inject);"); w.write("smithy::ClientConfig config;"); + w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries"); w.write("config.http_client = transport;"); w.write("auto client = *$LClient::Create(std::move(config));", name); w.write("Rng rng{std::mt19937{99U}, /*fill_all=*/true};"); diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ProtocolSupport.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ProtocolSupport.java index 47fea30c..d21638a8 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ProtocolSupport.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ProtocolSupport.java @@ -142,6 +142,62 @@ static String int64Bounds(software.amazon.smithy.model.shapes.ShapeType type) { }; } + /** + * Client-side @requestCompression: gzip the request body once it reaches the configured minimum + * size, appending to any member-bound Content-Encoding header. Emitted after the body and every + * header binding; Send() computes content-length afterwards. + */ + static void writeRequestCompression( + CppWriter w, software.amazon.smithy.model.shapes.OperationShape operation) { + var trait = + operation.getTrait(software.amazon.smithy.model.traits.RequestCompressionTrait.class); + if (trait.isEmpty() || !trait.get().getEncodings().contains("gzip")) { + return; + } + w.addInclude("\"smithy/compression/gzip.h\""); + w.addInclude(""); + w.write("// @requestCompression(gzip): applied last, appended to Content-Encoding."); + w.openBlock( + "if (request.body.size() >= " + + "static_cast(config_.request_min_compression_size_bytes)) {"); + w.write("auto compressed = smithy::GzipCompress(request.body);"); + w.write("if (!compressed) return std::move(compressed).error();"); + w.write("request.body = *std::move(compressed);"); + w.write("const auto existing_encoding = request.headers.Get(\"content-encoding\");"); + w.write( + "request.headers.Set(\"content-encoding\", existing_encoding.has_value() && " + + "!existing_encoding->empty() ? *existing_encoding + \", gzip\" : \"gzip\");"); + w.closeBlock("}"); + } + + /** + * Server-side inverse: transparently gunzip request bodies for @requestCompression operations + * when the (final) Content-Encoding is gzip. Emitted at the top of the route lambda. + */ + static void writeRequestDecompression( + CppWriter w, + software.amazon.smithy.model.shapes.OperationShape operation, + String errorFn, + String errorCode) { + var trait = + operation.getTrait(software.amazon.smithy.model.traits.RequestCompressionTrait.class); + if (trait.isEmpty() || !trait.get().getEncodings().contains("gzip")) { + return; + } + w.addInclude("\"smithy/compression/gzip.h\""); + w.write("// @requestCompression(gzip): decode before parsing."); + w.openBlock( + "if (const auto request_encoding = request.headers.Get(\"content-encoding\"); " + + "request_encoding.has_value() && (*request_encoding == \"gzip\" || " + + "request_encoding->ends_with(\", gzip\"))) {"); + w.write("auto decompressed = smithy::GzipDecompress(request.body);"); + w.openBlock("if (!decompressed) {"); + w.write("return $L(400, $S, \"invalid gzip request body\", {});", errorFn, errorCode); + w.closeBlock("}"); + w.write("request.body = *std::move(decompressed);"); + w.closeBlock("}"); + } + /** HTTP status for a modeled error shape: @httpError, else @error class default. */ static int errorStatus(StructureShape shape) { var httpError = shape.getTrait(software.amazon.smithy.model.traits.HttpErrorTrait.class); diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ProtocolTestGenerator.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ProtocolTestGenerator.java index 0b0d9513..a2d62fcd 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ProtocolTestGenerator.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/ProtocolTestGenerator.java @@ -410,6 +410,7 @@ private void writeFixture(CppWriter w) { w.openBlock("Fixture MakeFixture(const std::string& endpoint = \"\") {"); w.write("auto transport = std::make_shared();"); w.write("smithy::ClientConfig config;"); + w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries"); w.write("config.http_client = transport;"); w.write("config.endpoint = endpoint;"); w.write("// Create cannot fail when a transport is injected."); @@ -678,6 +679,7 @@ private void writeMinimalRequestHelper(CppWriter w, OperationShape operation) { w.openBlock("smithy::http::HttpRequest MinimalRequestFor$L() {", opName); w.write("auto transport = std::make_shared();"); w.write("smithy::ClientConfig config;"); + w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries"); w.write("config.http_client = transport;"); w.write("auto client = *$L::Create(std::move(config));", clientType()); w.writeWithNoFormatting( diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/RestJson1Protocol.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/RestJson1Protocol.java index 142a1021..d0b0db3c 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/RestJson1Protocol.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/RestJson1Protocol.java @@ -283,6 +283,7 @@ public void writeOperationBody( w.write( "request.headers.Set(\"accept\", $S);", payloadContentType(context, operation, false)); } + ProtocolSupport.writeRequestCompression(w, operation); w.write("auto response = Send(std::move(request));"); w.write("if (!response) return std::move(response).error();"); if (responseCode != null) { @@ -1313,11 +1314,21 @@ public void writeServerRoute( b -> b.getLocation() == HttpBinding.Location.DOCUMENT || b.getLocation() == HttpBinding.Location.PAYLOAD); + boolean compressed = + operation + .getTrait(software.amazon.smithy.model.traits.RequestCompressionTrait.class) + .map(t -> t.getEncodings().contains("gzip")) + .orElse(false); w.openBlock( - "(void)router_->Add($S, $S, [handler](const smithy::http::HttpRequest& request, " + "(void)router_->Add($S, $S, [handler](const smithy::http::HttpRequest& $L, " + "const smithy::server::RequestContext& context) -> smithy::http::HttpResponse {", http.getMethod(), - pattern.toString()); + pattern.toString(), + compressed ? "raw_request" : "request"); + if (compressed) { + w.write("smithy::http::HttpRequest request = raw_request;"); + ProtocolSupport.writeRequestDecompression(w, operation, "JsonError", ""); + } StructureShape inputShape = ProtocolSupport.inputShape(context, operation); boolean noModeledInput = inputShape.getId().toString().equals("smithy.api#Unit") diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/Rpcv2CborProtocol.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/Rpcv2CborProtocol.java index 1b6c6558..d5fef58c 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/Rpcv2CborProtocol.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/Rpcv2CborProtocol.java @@ -91,12 +91,23 @@ public void writeServerRoute( String inputType = context.cppSymbols().toSymbol(input).getName(); String opName = CppReservedWords.escape(operation.getId().getName()); + boolean compressed = + operation + .getTrait(software.amazon.smithy.model.traits.RequestCompressionTrait.class) + .map(t -> t.getEncodings().contains("gzip")) + .orElse(false); w.openBlock( "(void)router_->Add(\"POST\", \"/service/$L/operation/$L\", " - + "[handler](const smithy::http::HttpRequest& request, " + + "[handler](const smithy::http::HttpRequest& $L, " + "const smithy::server::RequestContext&) -> smithy::http::HttpResponse {", service.getId().getName(), - operation.getId().getName()); + operation.getId().getName(), + compressed ? "raw_request" : "request"); + if (compressed) { + w.write("smithy::http::HttpRequest request = raw_request;"); + ProtocolSupport.writeRequestDecompression( + w, operation, "CborError", "SerializationException"); + } w.openBlock( "if (request.headers.Get(\"smithy-protocol\").value_or(\"\") != \"rpc-v2-cbor\") {"); w.write( @@ -185,6 +196,7 @@ public void writeOperationBody( in); } + ProtocolSupport.writeRequestCompression(w, operation); w.write("auto response = Send(std::move(request));"); w.write("if (!response) return std::move(response).error();"); w.write( diff --git a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/SmokeTestGenerator.java b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/SmokeTestGenerator.java index e357f120..e1825426 100644 --- a/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/SmokeTestGenerator.java +++ b/codegen/smithy-cpp-codegen/src/main/java/io/smithycpp/codegen/SmokeTestGenerator.java @@ -183,6 +183,7 @@ private void writeSource(CppWriter w) { w.write("auto loopback = std::make_shared();"); w.write("(void)loopback->Start(server.Handler());"); w.write("smithy::ClientConfig config;"); + w.write("config.retry.max_attempts = 1; // wire-exact tests: no retries"); w.write("config.http_client = loopback;"); w.write("// Create cannot fail when a transport is injected."); w.write("return *$LClient::Create(std::move(config));", name); diff --git a/codegen/smithy-cpp-codegen/src/main/resources/io/smithycpp/codegen/protocol-test-exclusions.txt b/codegen/smithy-cpp-codegen/src/main/resources/io/smithycpp/codegen/protocol-test-exclusions.txt index 681baf08..ddda5bfa 100644 --- a/codegen/smithy-cpp-codegen/src/main/resources/io/smithycpp/codegen/protocol-test-exclusions.txt +++ b/codegen/smithy-cpp-codegen/src/main/resources/io/smithycpp/codegen/protocol-test-exclusions.txt @@ -25,9 +25,6 @@ aws.protocoltests.restjson#RestJson any RestJsonInputAndOutputWithQuotedStringHe # @httpChecksumRequired (Content-MD5) is not implemented yet. aws.protocoltests.restjson#RestJson request RestJsonHttpChecksumRequired @httpChecksumRequired is not implemented yet -# @requestCompression (gzip) is not implemented yet. -aws.protocoltests.restjson#RestJson request SDKAppliedContentEncoding_restJson1 @requestCompression is not implemented yet -aws.protocoltests.restjson#RestJson request SDKAppendedGzipAfterProvidedEncoding_restJson1 @requestCompression is not implemented yet # The test asserts an all-zero UUID, which needs an injectable token source. aws.protocoltests.restjson#RestJson request RestJsonQueryIdempotencyTokenAutoFill deterministic idempotency tokens need an injectable source @@ -48,9 +45,11 @@ aws.protocoltests.restjson#RestJson server-request RestJsonSupportsNaNFloatHeade aws.protocoltests.restjson#RestJson server-request RestJsonSupportsNaNFloatQueryValues NaN input members compare unequal under operator== smithy.protocoltests.rpcv2Cbor#RpcV2Protocol server-request RpcV2CborSupportsNaNFloatInputs NaN input members compare unequal under operator== -# @requestCompression (gzip) is not implemented yet. -aws.protocoltests.restjson#RestJson server-request SDKAppliedContentEncoding_restJson1 @requestCompression is not implemented yet -aws.protocoltests.restjson#RestJson server-request SDKAppendedGzipAfterProvidedEncoding_restJson1 @requestCompression is not implemented yet +# These cases define no wire body (client-only semantics: they assert the +# Content-Encoding header the client sends), so a server cannot parse the +# expected params from them. +aws.protocoltests.restjson#RestJson server-request SDKAppliedContentEncoding_restJson1 the case has no wire body for the server to parse +aws.protocoltests.restjson#RestJson server-request SDKAppendedGzipAfterProvidedEncoding_restJson1 the case has no wire body for the server to parse # Absent query list params deserialize as unset members, not engaged empty lists. aws.protocoltests.restjson#RestJson server-request RestJsonOmitsEmptyListQueryValues absent query lists stay unset (nullopt), not engaged empty lists diff --git a/docs/production-guide.md b/docs/production-guide.md new file mode 100644 index 00000000..dc80a81d --- /dev/null +++ b/docs/production-guide.md @@ -0,0 +1,95 @@ +# Production guide + +How to configure generated smithy-cpp clients and servers for production use: +timeouts, retries, and request compression. Every knob lives on +`smithy::ClientConfig` (`smithy/client/config.h`), so the guidance below +applies to every generated client the same way. + +```cpp +#include "myservice/client.h" +#include "smithy/client/config.h" + +smithy::ClientConfig config; +config.endpoint = "http://api.example.com:8080"; +config.request_timeout_ms = 5000; +config.retry.max_attempts = 5; +auto client = myservice::MyServiceClient::Create(std::move(config)); +``` + +## Timeouts + +`config.request_timeout_ms` (default 30000) bounds each HTTP attempt — +connect plus request plus response — on the built-in socket transport. A +timed-out attempt fails with a retryable transport error, so it feeds the +retry loop below. If you inject your own `http_client`, the transport owns +timeout enforcement; the built-in behavior is the reference. + +Pick a timeout from your service's latency tail (a small multiple of p99), +not a comfortable-sounding round number: with retries enabled the worst-case +caller wait is roughly `max_attempts × timeout` plus backoff sleeps. + +## Retries + +Every generated client sends through `smithy::SendWithRetries` +(`smithy/client/retry.h`). Two failure classes are retried: + +- **Transport errors flagged retryable** — connection refused/reset, + timeouts. +- **Transient HTTP statuses** — 429, 500, 502, 503, 504 (the set every + Smithy SDK treats as transient). Other statuses, including 400/403/404 and + modeled errors, are returned immediately. + +Backoff is **full-jitter exponential**: retry *n* sleeps +`uniform(0, min(max_backoff, initial_backoff × 2^(n-1)))`. Jitter +desynchronizes clients after a shared failure, so a recovering server is not +hit by a synchronized thundering herd. + +```cpp +config.retry.max_attempts = 3; // total tries; 1 disables retries +config.retry.initial_backoff = std::chrono::milliseconds(100); +config.retry.max_backoff = std::chrono::milliseconds(20000); +``` + +Guidance: + +- **Interactive paths:** keep `max_attempts` low (2–3) and cap + `max_backoff` near your latency budget; a user-facing call gains nothing + from a 20-second sleep. +- **Batch/background paths:** raise `max_attempts` and let `max_backoff` + breathe; throttling (429) resolves on its own if you back off. +- **Idempotency:** retries resend the same serialized request. + `@idempotencyToken` members are generated once per call and reused across + attempts, so the server can deduplicate. For non-idempotent operations + without a token, weigh whether a retried timeout can double-apply. +- **Tests:** wire-exact tests set `config.retry.max_attempts = 1` (generated + suites already do). To test retry behavior deterministically, inject + `config.retry.sleep` and `config.retry.jitter`. + +## Request compression + +Operations modeled with `@requestCompression(encodings: ["gzip"])` gzip +their request body when it reaches +`config.request_min_compression_size_bytes` (default 10240, the Smithy +default; 0 compresses everything). The client appends `gzip` to any existing +`Content-Encoding` header value. Nothing is configured per call — model the +trait and the generated client and server both handle it: + +- **Client:** compresses via `smithy::GzipCompress` (`//runtime:compression`, + zlib) after serialization, before send. +- **Server:** generated routes for `@requestCompression` operations + transparently gunzip requests arriving with `Content-Encoding: gzip` + (or `..., gzip`) and reject malformed gzip bodies with a 400 + serialization error. Decompression is capped (64 MB) to stop + decompression-bomb inputs. + +Compression trades CPU for bytes: leave the 10 KiB threshold alone unless +you have measured small-payload wins; compressing tiny bodies usually +inflates them. + +## Server hardening + +The production server transport (`BeastServerTransport`, ADR-0006) already +enforces per-connection timeouts, body-size limits, and graceful shutdown; +see [server-guide.md](server-guide.md). Phase 7b extends this area +(thread-pool sizing, drain, slow-client handling, logging/metrics hooks) — +see [PLAN.md](PLAN.md). diff --git a/docs/runtime.md b/docs/runtime.md index c23c1837..05c44ed1 100644 --- a/docs/runtime.md +++ b/docs/runtime.md @@ -12,7 +12,8 @@ crates (PLAN §3.2a). | `//runtime:cbor` | `smithy::cbor` | `Document` ⇄ deterministic CBOR (RFC 8949; tag-1 timestamps; tolerant decoder) — ADR-0005 | | `//runtime:http` | `smithy::http` | `Headers` (case-insensitive), URI percent-encoding per the Smithy HTTP binding rules, `HttpRequest`/`HttpResponse`, `HttpClient`/`HttpServerTransport` interfaces, `Loopback` in-memory transport, built-in `SocketHttpClient`/`SocketHttpServer` (test/reference only — ADR-0006) | | `//runtime:http_beast` | `smithy::http` | `BeastServerTransport` (ADR-0006): the production server transport on BCR modular Boost.Beast/asio — concurrent connections on a thread pool, keep-alive, per-connection timeouts, body-size limits, graceful shutdown. Separate target so Boost stays out of dep-light builds | -| `//runtime:client` | `smithy` | `ClientConfig` (endpoint, timeout, user-agent, transport injection) | +| `//runtime:client` | `smithy` | `ClientConfig` (endpoint, timeout, user-agent, transport injection, `RetryPolicy`, request-compression threshold), `SendWithRetries` (full-jitter exponential backoff over transport errors and 429/5xx — see docs/production-guide.md) | +| `//runtime:compression` | `smithy` | `GzipCompress`/`GzipDecompress` (zlib; decompression-bomb guard, trailing-garbage rejection) backing `@requestCompression` | | `//runtime:server` | `smithy::server` | `Router` (literal > label > greedy precedence, 404/405/400), `RequestContext`, `MakeErrorResponse`, `ValidationFailure` | ## Design rules diff --git a/docs/server-guide.md b/docs/server-guide.md index 2fe41b70..fc1b6f3c 100644 --- a/docs/server-guide.md +++ b/docs/server-guide.md @@ -85,11 +85,13 @@ timestamps, single-member unions, `SerializationException`/`UnsupportedMediaType responses — `@httpResponseCode`, 204 No Content bodies suppressed, Content-Type (415) and Accept (406) enforcement (blob payloads without `@mediaType` accept anything), and all-query-params `@httpQueryParams` maps. Ambiguous route tables fail at generation time. +Routes for `@requestCompression` operations transparently gunzip request bodies arriving with +`Content-Encoding: gzip` (decompression is size-capped against bombs; malformed gzip is a 400 +serialization error) — see [production-guide.md](production-guide.md). ## Not yet generated (Phase 5+) Nested `@required` absences as `fieldList` entries and a server-strict serde variant (clients must skip null dense-map values and accept UTC-offset timestamps in responses; servers share that serde today), ReDoS-safe `@pattern` matching (`std::regex` backtracks, so the suite's -deliberately catastrophic pattern is excluded), `@streaming` payloads (Phase 8), and -`@requestCompression`. +deliberately catastrophic pattern is excluded), and `@streaming` payloads (Phase 8). diff --git a/examples/bazel-consumer/MODULE.bazel.lock b/examples/bazel-consumer/MODULE.bazel.lock index 222d9af1..b03da50c 100644 --- a/examples/bazel-consumer/MODULE.bazel.lock +++ b/examples/bazel-consumer/MODULE.bazel.lock @@ -311,7 +311,8 @@ "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.8/MODULE.bazel": "772c674bb78a0342b8caf32ab5c25085c493ca4ff08398208dcbe4375fe9f776", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.8/source.json": "cf377d76800dfc3d3b71e9dd4a8c53a62837cbce37cc4f25e6207b15fc1e8f2b", "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" }, "selectedYankedVersions": {}, diff --git a/examples/cafe/generated/src/client.cc b/examples/cafe/generated/src/client.cc index 28a17437..5292c2d2 100644 --- a/examples/cafe/generated/src/client.cc +++ b/examples/cafe/generated/src/client.cc @@ -121,7 +121,7 @@ smithy::Outcome CafeClient::Send(smithy::http::HttpR if (!request.body.empty()) { request.headers.Set("content-length", std::to_string(request.body.size())); } - return transport_->Send(request); + return smithy::SendWithRetries(*transport_, request, config_.retry); } smithy::Outcome CafeClient::GetOrder(const GetOrderInput& input) const { diff --git a/examples/cafe/generated/tests/integration_test.cc b/examples/cafe/generated/tests/integration_test.cc index 739370d6..df726d19 100644 --- a/examples/cafe/generated/tests/integration_test.cc +++ b/examples/cafe/generated/tests/integration_test.cc @@ -182,6 +182,7 @@ class CafeIntegrationTest : public ::testing::TestWithParam { handler_ = std::make_shared(); server_ = std::make_unique(handler_); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries if (GetParam() == TransportKind::kLoopback) { auto loopback = std::make_shared(); ASSERT_TRUE(loopback->Start(server_->Handler()).ok()); @@ -300,6 +301,7 @@ TEST(CafeIntegrationUnknownMembers, GetOrderToleratesUnknownResponseMembers) { }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *CafeClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; @@ -325,6 +327,7 @@ TEST(CafeIntegrationUnknownMembers, OrderCoffeeToleratesUnknownResponseMembers) }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *CafeClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; diff --git a/examples/cafe/generated/tests/smoke_test.cc b/examples/cafe/generated/tests/smoke_test.cc index 3404cf4b..195ef44b 100644 --- a/examples/cafe/generated/tests/smoke_test.cc +++ b/examples/cafe/generated/tests/smoke_test.cc @@ -60,6 +60,7 @@ CafeClient MakeClient(std::shared_ptr handler) { auto loopback = std::make_shared(); (void)loopback->Start(server.Handler()); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = loopback; // Create cannot fail when a transport is injected. return *CafeClient::Create(std::move(config)); diff --git a/examples/roundtrip/rest/generated/src/client.cc b/examples/roundtrip/rest/generated/src/client.cc index dfb08d6a..7bd222bd 100644 --- a/examples/roundtrip/rest/generated/src/client.cc +++ b/examples/roundtrip/rest/generated/src/client.cc @@ -174,7 +174,7 @@ smithy::Outcome RoundTripRestClient::Send(smithy::ht if (!request.body.empty()) { request.headers.Set("content-length", std::to_string(request.body.size())); } - return transport_->Send(request); + return smithy::SendWithRetries(*transport_, request, config_.retry); } smithy::Outcome RoundTripRestClient::DescribeSink(const DescribeSinkInput& input) const { diff --git a/examples/roundtrip/rest/generated/tests/integration_test.cc b/examples/roundtrip/rest/generated/tests/integration_test.cc index 104cb605..cd725f19 100644 --- a/examples/roundtrip/rest/generated/tests/integration_test.cc +++ b/examples/roundtrip/rest/generated/tests/integration_test.cc @@ -248,6 +248,7 @@ class RoundTripRestIntegrationTest : public ::testing::TestWithParam(); server_ = std::make_unique(handler_); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries if (GetParam() == TransportKind::kLoopback) { auto loopback = std::make_shared(); ASSERT_TRUE(loopback->Start(server_->Handler()).ok()); @@ -422,6 +423,7 @@ TEST(RoundTripRestIntegrationUnknownMembers, DescribeSinkToleratesUnknownRespons }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RoundTripRestClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; @@ -447,6 +449,7 @@ TEST(RoundTripRestIntegrationUnknownMembers, PutSinkToleratesUnknownResponseMemb }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RoundTripRestClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; diff --git a/examples/roundtrip/rest/generated/tests/smoke_test.cc b/examples/roundtrip/rest/generated/tests/smoke_test.cc index 9e031dda..9322076f 100644 --- a/examples/roundtrip/rest/generated/tests/smoke_test.cc +++ b/examples/roundtrip/rest/generated/tests/smoke_test.cc @@ -60,6 +60,7 @@ RoundTripRestClient MakeClient(std::shared_ptr handler) { auto loopback = std::make_shared(); (void)loopback->Start(server.Handler()); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = loopback; // Create cannot fail when a transport is injected. return *RoundTripRestClient::Create(std::move(config)); diff --git a/examples/roundtrip/rpc/generated/src/client.cc b/examples/roundtrip/rpc/generated/src/client.cc index 77490bc4..55364937 100644 --- a/examples/roundtrip/rpc/generated/src/client.cc +++ b/examples/roundtrip/rpc/generated/src/client.cc @@ -116,7 +116,7 @@ smithy::Outcome RoundTripRpcClient::Send(smithy::htt if (!request.body.empty()) { request.headers.Set("content-length", std::to_string(request.body.size())); } - return transport_->Send(request); + return smithy::SendWithRetries(*transport_, request, config_.retry); } smithy::Outcome RoundTripRpcClient::PutSinkRpc(const PutSinkRpcInput& input) const { diff --git a/examples/roundtrip/rpc/generated/tests/integration_test.cc b/examples/roundtrip/rpc/generated/tests/integration_test.cc index c61f382c..bee9ac7f 100644 --- a/examples/roundtrip/rpc/generated/tests/integration_test.cc +++ b/examples/roundtrip/rpc/generated/tests/integration_test.cc @@ -192,6 +192,7 @@ class RoundTripRpcIntegrationTest : public ::testing::TestWithParam(); server_ = std::make_unique(handler_); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries if (GetParam() == TransportKind::kLoopback) { auto loopback = std::make_shared(); ASSERT_TRUE(loopback->Start(server_->Handler()).ok()); @@ -284,6 +285,7 @@ TEST(RoundTripRpcIntegrationUnknownMembers, PutSinkRpcToleratesUnknownResponseMe }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RoundTripRpcClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; diff --git a/examples/roundtrip/rpc/generated/tests/smoke_test.cc b/examples/roundtrip/rpc/generated/tests/smoke_test.cc index f7225db3..5c5ca0ad 100644 --- a/examples/roundtrip/rpc/generated/tests/smoke_test.cc +++ b/examples/roundtrip/rpc/generated/tests/smoke_test.cc @@ -38,6 +38,7 @@ RoundTripRpcClient MakeClient(std::shared_ptr handler) { auto loopback = std::make_shared(); (void)loopback->Start(server.Handler()); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = loopback; // Create cannot fail when a transport is injected. return *RoundTripRpcClient::Create(std::move(config)); diff --git a/examples/weather/BUILD.bazel b/examples/weather/BUILD.bazel index 74c91e5c..59dfc0eb 100644 --- a/examples/weather/BUILD.bazel +++ b/examples/weather/BUILD.bazel @@ -53,7 +53,9 @@ cc_test( srcs = ["generated_server_e2e_test.cc"], deps = [ ":weather_handwritten", + "//examples/weather/generated:client", "//examples/weather/generated:server", + "//runtime:client", "//runtime:http", "@googletest//:gtest_main", ], diff --git a/examples/weather/generated/src/client.cc b/examples/weather/generated/src/client.cc index 983131ce..17747b2c 100644 --- a/examples/weather/generated/src/client.cc +++ b/examples/weather/generated/src/client.cc @@ -158,7 +158,7 @@ smithy::Outcome WeatherClient::Send(smithy::http::Ht if (!request.body.empty()) { request.headers.Set("content-length", std::to_string(request.body.size())); } - return transport_->Send(request); + return smithy::SendWithRetries(*transport_, request, config_.retry); } smithy::Outcome WeatherClient::DeleteCity(const DeleteCityInput& input) const { diff --git a/examples/weather/generated/tests/integration_test.cc b/examples/weather/generated/tests/integration_test.cc index cf28c11c..0e35da13 100644 --- a/examples/weather/generated/tests/integration_test.cc +++ b/examples/weather/generated/tests/integration_test.cc @@ -202,6 +202,7 @@ class WeatherIntegrationTest : public ::testing::TestWithParam { handler_ = std::make_shared(); server_ = std::make_unique(handler_); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries if (GetParam() == TransportKind::kLoopback) { auto loopback = std::make_shared(); ASSERT_TRUE(loopback->Start(server_->Handler()).ok()); @@ -413,6 +414,7 @@ TEST(WeatherIntegrationUnknownMembers, GetCityToleratesUnknownResponseMembers) { }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *WeatherClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; @@ -438,6 +440,7 @@ TEST(WeatherIntegrationUnknownMembers, GetCurrentTimeToleratesUnknownResponseMem }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *WeatherClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; @@ -463,6 +466,7 @@ TEST(WeatherIntegrationUnknownMembers, GetForecastToleratesUnknownResponseMember }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *WeatherClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; @@ -488,6 +492,7 @@ TEST(WeatherIntegrationUnknownMembers, ListCitiesToleratesUnknownResponseMembers }; auto transport = std::make_shared(loopback, inject); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *WeatherClient::Create(std::move(config)); Rng rng{std::mt19937{99U}, /*fill_all=*/true}; diff --git a/examples/weather/generated/tests/smoke_test.cc b/examples/weather/generated/tests/smoke_test.cc index 5149c81f..f8e420a0 100644 --- a/examples/weather/generated/tests/smoke_test.cc +++ b/examples/weather/generated/tests/smoke_test.cc @@ -82,6 +82,7 @@ WeatherClient MakeClient(std::shared_ptr handler) { auto loopback = std::make_shared(); (void)loopback->Start(server.Handler()); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = loopback; // Create cannot fail when a transport is injected. return *WeatherClient::Create(std::move(config)); diff --git a/examples/weather/generated_server_e2e_test.cc b/examples/weather/generated_server_e2e_test.cc index afeadfb4..64506811 100644 --- a/examples/weather/generated_server_e2e_test.cc +++ b/examples/weather/generated_server_e2e_test.cc @@ -4,8 +4,10 @@ #include +#include #include +#include "example/weather/client.h" #include "example/weather/server.h" #include "examples/weather/handwritten/weather_client.h" #include "smithy/http/loopback.h" @@ -88,6 +90,37 @@ TEST_F(GeneratedServerEndToEndTest, GetCityRoundTrips) { EXPECT_FLOAT_EQ(city->coordinates.longitude, -122.3321F); } +// A transport that fails transiently proves the generated client's retry +// path end to end (Phase 7): the third attempt reaches the server. +class FlakyTransport final : public smithy::http::HttpClient { + public: + explicit FlakyTransport(std::shared_ptr inner) + : inner_(std::move(inner)) {} + + smithy::Outcome Send( + const smithy::http::HttpRequest& request) override { + if (++calls_ <= 2) return smithy::Error::Transport("transient outage"); + return inner_->Send(request); + } + + private: + std::shared_ptr inner_; + int calls_ = 0; +}; + +TEST_F(GeneratedServerEndToEndTest, GeneratedClientRetriesTransientFailures) { + auto loopback = std::make_shared(); + ASSERT_TRUE(loopback->Start(server_->Handler()).ok()); + smithy::ClientConfig config; + config.http_client = std::make_shared(loopback); + config.retry.sleep = [](std::chrono::milliseconds) {}; // instant for tests + auto client = example::weather::WeatherClient::Create(std::move(config)); + ASSERT_TRUE(client.ok()); + const auto city = client->GetCity(example::weather::GetCityInput{.cityId = "seattle"}); + ASSERT_TRUE(city.ok()) << city.error().message(); + EXPECT_EQ(city->name, "Seattle"); +} + TEST_F(GeneratedServerEndToEndTest, DeleteCityIs204WithNoBody) { smithy::http::HttpRequest request; request.method = "DELETE"; diff --git a/protocol-tests/restjson1-validation/generated/BUILD.bazel b/protocol-tests/restjson1-validation/generated/BUILD.bazel index 50bcb1f1..ac82329a 100644 --- a/protocol-tests/restjson1-validation/generated/BUILD.bazel +++ b/protocol-tests/restjson1-validation/generated/BUILD.bazel @@ -31,6 +31,7 @@ cc_library( ":serde", ":types", "//runtime:client", + "//runtime:compression", "//runtime:core", "//runtime:http", "//runtime:json", @@ -45,6 +46,7 @@ cc_library( deps = [ ":serde", ":types", + "//runtime:compression", "//runtime:core", "//runtime:http", "//runtime:json", diff --git a/protocol-tests/restjson1-validation/generated/src/client.cc b/protocol-tests/restjson1-validation/generated/src/client.cc index ec1c3bcb..af38597c 100644 --- a/protocol-tests/restjson1-validation/generated/src/client.cc +++ b/protocol-tests/restjson1-validation/generated/src/client.cc @@ -206,7 +206,7 @@ smithy::Outcome RestJsonValidationClient::Send(smith if (!request.body.empty()) { request.headers.Set("content-length", std::to_string(request.body.size())); } - return transport_->Send(request); + return smithy::SendWithRetries(*transport_, request, config_.retry); } smithy::Outcome RestJsonValidationClient::MalformedEnum(const MalformedEnumInput& input) const { diff --git a/protocol-tests/restjson1-validation/generated/tests/smoke_test.cc b/protocol-tests/restjson1-validation/generated/tests/smoke_test.cc index e1c5e371..a5b72e84 100644 --- a/protocol-tests/restjson1-validation/generated/tests/smoke_test.cc +++ b/protocol-tests/restjson1-validation/generated/tests/smoke_test.cc @@ -148,6 +148,7 @@ RestJsonValidationClient MakeClient(std::shared_ptr h auto loopback = std::make_shared(); (void)loopback->Start(server.Handler()); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = loopback; // Create cannot fail when a transport is injected. return *RestJsonValidationClient::Create(std::move(config)); diff --git a/protocol-tests/restjson1/generated/BUILD.bazel b/protocol-tests/restjson1/generated/BUILD.bazel index 4a020c8f..bfb4b27b 100644 --- a/protocol-tests/restjson1/generated/BUILD.bazel +++ b/protocol-tests/restjson1/generated/BUILD.bazel @@ -31,6 +31,7 @@ cc_library( ":serde", ":types", "//runtime:client", + "//runtime:compression", "//runtime:core", "//runtime:http", "//runtime:json", @@ -45,6 +46,7 @@ cc_library( deps = [ ":serde", ":types", + "//runtime:compression", "//runtime:core", "//runtime:http", "//runtime:json", diff --git a/protocol-tests/restjson1/generated/src/client.cc b/protocol-tests/restjson1/generated/src/client.cc index 82ab9d66..75d22b1b 100644 --- a/protocol-tests/restjson1/generated/src/client.cc +++ b/protocol-tests/restjson1/generated/src/client.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -10,6 +11,7 @@ #include #include +#include "smithy/compression/gzip.h" #include "smithy/core/base64.h" #include "smithy/core/blob.h" #include "smithy/core/document_serde.h" @@ -175,7 +177,7 @@ smithy::Outcome RestJsonClient::Send(smithy::http::H if (!request.body.empty()) { request.headers.Set("content-length", std::to_string(request.body.size())); } - return transport_->Send(request); + return smithy::SendWithRetries(*transport_, request, config_.retry); } smithy::Outcome RestJsonClient::AllQueryStringTypes(const AllQueryStringTypesInput& input) const { @@ -2237,6 +2239,14 @@ smithy::Outcome RestJsonClient::PutWithContentEnco } request.body = smithy::json::Encode(smithy::Document(std::move(body_map))); request.headers.Set("content-type", "application/json"); + // @requestCompression(gzip): applied last, appended to Content-Encoding. + if (request.body.size() >= static_cast(config_.request_min_compression_size_bytes)) { + auto compressed = smithy::GzipCompress(request.body); + if (!compressed) return std::move(compressed).error(); + request.body = *std::move(compressed); + const auto existing_encoding = request.headers.Get("content-encoding"); + request.headers.Set("content-encoding", existing_encoding.has_value() && !existing_encoding->empty() ? *existing_encoding + ", gzip" : "gzip"); + } auto response = Send(std::move(request)); if (!response) return std::move(response).error(); if (response->status != 200) return GenericError(ParseError(*response)); diff --git a/protocol-tests/restjson1/generated/src/server.cc b/protocol-tests/restjson1/generated/src/server.cc index 367956d5..779637e6 100644 --- a/protocol-tests/restjson1/generated/src/server.cc +++ b/protocol-tests/restjson1/generated/src/server.cc @@ -13,6 +13,7 @@ #include #include +#include "smithy/compression/gzip.h" #include "smithy/core/base64.h" #include "smithy/core/blob.h" #include "smithy/core/document.h" @@ -6235,7 +6236,16 @@ RestJsonServer::RestJsonServer(std::shared_ptr handler) if (!outcome) return ErrorToResponse(outcome.error()); return SerializePostUnionWithJsonNameResponse(*outcome); }); - (void)router_->Add("POST", "/requestcompression/putcontentwithencoding", [handler](const smithy::http::HttpRequest& request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { + (void)router_->Add("POST", "/requestcompression/putcontentwithencoding", [handler](const smithy::http::HttpRequest& raw_request, const smithy::server::RequestContext& context) -> smithy::http::HttpResponse { + smithy::http::HttpRequest request = raw_request; + // @requestCompression(gzip): decode before parsing. + if (const auto request_encoding = request.headers.Get("content-encoding"); request_encoding.has_value() && (*request_encoding == "gzip" || request_encoding->ends_with(", gzip"))) { + auto decompressed = smithy::GzipDecompress(request.body); + if (!decompressed) { + return JsonError(400, "", "invalid gzip request body", {}); + } + request.body = *std::move(decompressed); + } // Content-Type validation per the HTTP binding spec (415), then Accept (406); // the malformed-request suite pins the error-identity headers. A missing // content-type is tolerated, and blob payloads without @mediaType accept diff --git a/protocol-tests/restjson1/generated/tests/request_tests.cc b/protocol-tests/restjson1/generated/tests/request_tests.cc index 1a3cd98a..b8316f9c 100644 --- a/protocol-tests/restjson1/generated/tests/request_tests.cc +++ b/protocol-tests/restjson1/generated/tests/request_tests.cc @@ -20,8 +20,6 @@ namespace smithy::protocoltests::restjson { // RestJsonInputAndOutputWithQuotedStringHeaders (request) — quoted-string list headers are not implemented yet // RestJsonClientPopulatesDefaultValuesInInput (request) — @default population is not implemented yet // RestJsonClientPopulatesNestedDefaultValuesWhenMissing (request) — @default population is not implemented yet -// SDKAppliedContentEncoding_restJson1 (request) — @requestCompression is not implemented yet -// SDKAppendedGzipAfterProvidedEncoding_restJson1 (request) — @requestCompression is not implemented yet // RestJsonQueryIdempotencyTokenAutoFill (request) — deterministic idempotency tokens need an injectable source namespace { @@ -34,6 +32,7 @@ struct Fixture { Fixture MakeFixture(const std::string& endpoint = "") { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; config.endpoint = endpoint; // Create cannot fail when a transport is injected. @@ -1714,6 +1713,39 @@ TEST(RestJsonRequestTest, PostUnionWithJsonNameRequest3) { EXPECT_TRUE(smithy::testing::JsonBodyEquals("{\n \"value\": {\n \"bar\": \"hi\"\n }\n}", request.body)); } +// Compression algorithm encoding is appended to the Content-Encoding header. +TEST(RestJsonRequestTest, SDKAppliedContentEncoding_restJson1) { + Fixture fixture = MakeFixture(); + const PutWithContentEncodingInput input = [] { + PutWithContentEncodingInput v{}; + v.data = "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\n"; + return v; +}(); + (void)fixture.client.PutWithContentEncoding(input); + const smithy::http::HttpRequest& request = fixture.transport->last_request; + EXPECT_EQ(request.method, "POST"); + EXPECT_EQ(smithy::testing::UriPath(request.target), "/requestcompression/putcontentwithencoding"); + EXPECT_EQ(request.headers.Get("Content-Encoding").value_or(""), "gzip"); +} + +// Compression algorithm encoding is appended to the Content-Encoding header, and the +// user-provided content-encoding is in the Content-Encoding header before the +// request compression encoding from the HTTP binding. +TEST(RestJsonRequestTest, SDKAppendedGzipAfterProvidedEncoding_restJson1) { + Fixture fixture = MakeFixture(); + const PutWithContentEncodingInput input = [] { + PutWithContentEncodingInput v{}; + v.encoding = "custom"; + v.data = "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\n"; + return v; +}(); + (void)fixture.client.PutWithContentEncoding(input); + const smithy::http::HttpRequest& request = fixture.transport->last_request; + EXPECT_EQ(request.method, "POST"); + EXPECT_EQ(smithy::testing::UriPath(request.target), "/requestcompression/putcontentwithencoding"); + EXPECT_EQ(request.headers.Get("Content-Encoding").value_or(""), "custom, gzip"); +} + // Uses the given idempotency token as-is TEST(RestJsonRequestTest, RestJsonQueryIdempotencyTokenAutoFillIsSet) { Fixture fixture = MakeFixture(); diff --git a/protocol-tests/restjson1/generated/tests/response_tests.cc b/protocol-tests/restjson1/generated/tests/response_tests.cc index 78a0412d..814390c7 100644 --- a/protocol-tests/restjson1/generated/tests/response_tests.cc +++ b/protocol-tests/restjson1/generated/tests/response_tests.cc @@ -33,6 +33,7 @@ struct Fixture { Fixture MakeFixture(const std::string& endpoint = "") { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; config.endpoint = endpoint; // Create cannot fail when a transport is injected. diff --git a/protocol-tests/restjson1/generated/tests/server_request_tests.cc b/protocol-tests/restjson1/generated/tests/server_request_tests.cc index bf42eef4..79011c08 100644 --- a/protocol-tests/restjson1/generated/tests/server_request_tests.cc +++ b/protocol-tests/restjson1/generated/tests/server_request_tests.cc @@ -26,8 +26,8 @@ namespace smithy::protocoltests::restjson { // RestJsonOmitsEmptyListQueryValues (server-request) — absent query lists stay unset (nullopt), not engaged empty lists // RestJsonServerPopulatesDefaultsWhenMissingInRequestBody (server-request) — @default population is not implemented yet // RestJsonServerPopulatesNestedDefaultsWhenMissingInRequestBody (server-request) — @default population is not implemented yet -// SDKAppliedContentEncoding_restJson1 (server-request) — @requestCompression is not implemented yet -// SDKAppendedGzipAfterProvidedEncoding_restJson1 (server-request) — @requestCompression is not implemented yet +// SDKAppliedContentEncoding_restJson1 (server-request) — the case has no wire body for the server to parse +// SDKAppendedGzipAfterProvidedEncoding_restJson1 (server-request) — the case has no wire body for the server to parse // RestJsonSupportsNaNFloatInputs (server-request) — NaN input members compare unequal under operator== namespace { diff --git a/protocol-tests/restjson1/generated/tests/server_response_tests.cc b/protocol-tests/restjson1/generated/tests/server_response_tests.cc index eb8cc52b..9b811bf7 100644 --- a/protocol-tests/restjson1/generated/tests/server_response_tests.cc +++ b/protocol-tests/restjson1/generated/tests/server_response_tests.cc @@ -1234,6 +1234,7 @@ class RecordingHandler : public RestJsonHandler { smithy::http::HttpRequest MinimalRequestForDocumentType() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); DocumentTypeInput input = [] { @@ -1247,6 +1248,7 @@ smithy::http::HttpRequest MinimalRequestForDocumentType() { smithy::http::HttpRequest MinimalRequestForDocumentTypeAsMapValue() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); DocumentTypeAsMapValueInput input = [] { @@ -1260,6 +1262,7 @@ smithy::http::HttpRequest MinimalRequestForDocumentTypeAsMapValue() { smithy::http::HttpRequest MinimalRequestForDocumentTypeAsPayload() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); DocumentTypeAsPayloadInput input = [] { @@ -1273,6 +1276,7 @@ smithy::http::HttpRequest MinimalRequestForDocumentTypeAsPayload() { smithy::http::HttpRequest MinimalRequestForEmptyInputAndEmptyOutput() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); EmptyInputAndEmptyOutputInput input = [] { @@ -1286,6 +1290,7 @@ smithy::http::HttpRequest MinimalRequestForEmptyInputAndEmptyOutput() { smithy::http::HttpRequest MinimalRequestForGreetingWithErrors() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); GreetingWithErrorsInput input = [] { @@ -1299,6 +1304,7 @@ smithy::http::HttpRequest MinimalRequestForGreetingWithErrors() { smithy::http::HttpRequest MinimalRequestForHttpEnumPayload() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpEnumPayloadInput input = [] { @@ -1312,6 +1318,7 @@ smithy::http::HttpRequest MinimalRequestForHttpEnumPayload() { smithy::http::HttpRequest MinimalRequestForHttpPayloadTraits() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpPayloadTraitsInput input = [] { @@ -1325,6 +1332,7 @@ smithy::http::HttpRequest MinimalRequestForHttpPayloadTraits() { smithy::http::HttpRequest MinimalRequestForHttpPayloadTraitsWithMediaType() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpPayloadTraitsWithMediaTypeInput input = [] { @@ -1338,6 +1346,7 @@ smithy::http::HttpRequest MinimalRequestForHttpPayloadTraitsWithMediaType() { smithy::http::HttpRequest MinimalRequestForHttpPayloadWithStructure() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpPayloadWithStructureInput input = [] { @@ -1351,6 +1360,7 @@ smithy::http::HttpRequest MinimalRequestForHttpPayloadWithStructure() { smithy::http::HttpRequest MinimalRequestForHttpPayloadWithUnion() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpPayloadWithUnionInput input = [] { @@ -1364,6 +1374,7 @@ smithy::http::HttpRequest MinimalRequestForHttpPayloadWithUnion() { smithy::http::HttpRequest MinimalRequestForHttpPrefixHeaders() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpPrefixHeadersInput input = [] { @@ -1377,6 +1388,7 @@ smithy::http::HttpRequest MinimalRequestForHttpPrefixHeaders() { smithy::http::HttpRequest MinimalRequestForHttpPrefixHeadersInResponse() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpPrefixHeadersInResponseInput input = [] { @@ -1390,6 +1402,7 @@ smithy::http::HttpRequest MinimalRequestForHttpPrefixHeadersInResponse() { smithy::http::HttpRequest MinimalRequestForHttpResponseCode() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpResponseCodeInput input = [] { @@ -1403,6 +1416,7 @@ smithy::http::HttpRequest MinimalRequestForHttpResponseCode() { smithy::http::HttpRequest MinimalRequestForHttpStringPayload() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); HttpStringPayloadInput input = [] { @@ -1416,6 +1430,7 @@ smithy::http::HttpRequest MinimalRequestForHttpStringPayload() { smithy::http::HttpRequest MinimalRequestForIgnoreQueryParamsInResponse() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); IgnoreQueryParamsInResponseInput input = [] { @@ -1429,6 +1444,7 @@ smithy::http::HttpRequest MinimalRequestForIgnoreQueryParamsInResponse() { smithy::http::HttpRequest MinimalRequestForInputAndOutputWithHeaders() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); InputAndOutputWithHeadersInput input = [] { @@ -1442,6 +1458,7 @@ smithy::http::HttpRequest MinimalRequestForInputAndOutputWithHeaders() { smithy::http::HttpRequest MinimalRequestForJsonBlobs() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); JsonBlobsInput input = [] { @@ -1455,6 +1472,7 @@ smithy::http::HttpRequest MinimalRequestForJsonBlobs() { smithy::http::HttpRequest MinimalRequestForJsonEnums() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); JsonEnumsInput input = [] { @@ -1468,6 +1486,7 @@ smithy::http::HttpRequest MinimalRequestForJsonEnums() { smithy::http::HttpRequest MinimalRequestForJsonIntEnums() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); JsonIntEnumsInput input = [] { @@ -1481,6 +1500,7 @@ smithy::http::HttpRequest MinimalRequestForJsonIntEnums() { smithy::http::HttpRequest MinimalRequestForJsonLists() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); JsonListsInput input = [] { @@ -1494,6 +1514,7 @@ smithy::http::HttpRequest MinimalRequestForJsonLists() { smithy::http::HttpRequest MinimalRequestForJsonMaps() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); JsonMapsInput input = [] { @@ -1507,6 +1528,7 @@ smithy::http::HttpRequest MinimalRequestForJsonMaps() { smithy::http::HttpRequest MinimalRequestForJsonTimestamps() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); JsonTimestampsInput input = [] { @@ -1520,6 +1542,7 @@ smithy::http::HttpRequest MinimalRequestForJsonTimestamps() { smithy::http::HttpRequest MinimalRequestForJsonUnions() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); JsonUnionsInput input = [] { @@ -1533,6 +1556,7 @@ smithy::http::HttpRequest MinimalRequestForJsonUnions() { smithy::http::HttpRequest MinimalRequestForMediaTypeHeader() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); MediaTypeHeaderInput input = [] { @@ -1546,6 +1570,7 @@ smithy::http::HttpRequest MinimalRequestForMediaTypeHeader() { smithy::http::HttpRequest MinimalRequestForNoInputAndNoOutput() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); NoInputAndNoOutputInput input = [] { @@ -1559,6 +1584,7 @@ smithy::http::HttpRequest MinimalRequestForNoInputAndNoOutput() { smithy::http::HttpRequest MinimalRequestForNoInputAndOutput() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); NoInputAndOutputInput input = [] { @@ -1572,6 +1598,7 @@ smithy::http::HttpRequest MinimalRequestForNoInputAndOutput() { smithy::http::HttpRequest MinimalRequestForNullAndEmptyHeadersServer() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); NullAndEmptyHeadersServerInput input = [] { @@ -1585,6 +1612,7 @@ smithy::http::HttpRequest MinimalRequestForNullAndEmptyHeadersServer() { smithy::http::HttpRequest MinimalRequestForPostPlayerAction() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); PostPlayerActionInput input = [] { @@ -1598,6 +1626,7 @@ smithy::http::HttpRequest MinimalRequestForPostPlayerAction() { smithy::http::HttpRequest MinimalRequestForPostUnionWithJsonName() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); PostUnionWithJsonNameInput input = [] { @@ -1611,6 +1640,7 @@ smithy::http::HttpRequest MinimalRequestForPostUnionWithJsonName() { smithy::http::HttpRequest MinimalRequestForResponseCodeHttpFallback() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); ResponseCodeHttpFallbackInput input = [] { @@ -1624,6 +1654,7 @@ smithy::http::HttpRequest MinimalRequestForResponseCodeHttpFallback() { smithy::http::HttpRequest MinimalRequestForResponseCodeRequired() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); ResponseCodeRequiredInput input = [] { @@ -1637,6 +1668,7 @@ smithy::http::HttpRequest MinimalRequestForResponseCodeRequired() { smithy::http::HttpRequest MinimalRequestForSimpleScalarProperties() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); SimpleScalarPropertiesInput input = [] { @@ -1650,6 +1682,7 @@ smithy::http::HttpRequest MinimalRequestForSimpleScalarProperties() { smithy::http::HttpRequest MinimalRequestForSparseJsonLists() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); SparseJsonListsInput input = [] { @@ -1663,6 +1696,7 @@ smithy::http::HttpRequest MinimalRequestForSparseJsonLists() { smithy::http::HttpRequest MinimalRequestForSparseJsonMaps() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); SparseJsonMapsInput input = [] { @@ -1676,6 +1710,7 @@ smithy::http::HttpRequest MinimalRequestForSparseJsonMaps() { smithy::http::HttpRequest MinimalRequestForTimestampFormatHeaders() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); TimestampFormatHeadersInput input = [] { @@ -1689,6 +1724,7 @@ smithy::http::HttpRequest MinimalRequestForTimestampFormatHeaders() { smithy::http::HttpRequest MinimalRequestForUnitInputAndOutput() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RestJsonClient::Create(std::move(config)); UnitInputAndOutputInput input = [] { diff --git a/protocol-tests/restjson1/generated/tests/smoke_test.cc b/protocol-tests/restjson1/generated/tests/smoke_test.cc index 5e61d2d3..7968230f 100644 --- a/protocol-tests/restjson1/generated/tests/smoke_test.cc +++ b/protocol-tests/restjson1/generated/tests/smoke_test.cc @@ -1135,6 +1135,7 @@ RestJsonClient MakeClient(std::shared_ptr handler) { auto loopback = std::make_shared(); (void)loopback->Start(server.Handler()); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = loopback; // Create cannot fail when a transport is injected. return *RestJsonClient::Create(std::move(config)); diff --git a/protocol-tests/rpcv2cbor/generated/BUILD.bazel b/protocol-tests/rpcv2cbor/generated/BUILD.bazel index 497a6189..61b9ed49 100644 --- a/protocol-tests/rpcv2cbor/generated/BUILD.bazel +++ b/protocol-tests/rpcv2cbor/generated/BUILD.bazel @@ -32,6 +32,7 @@ cc_library( ":types", "//runtime:cbor", "//runtime:client", + "//runtime:compression", "//runtime:core", "//runtime:http", ], @@ -46,6 +47,7 @@ cc_library( ":serde", ":types", "//runtime:cbor", + "//runtime:compression", "//runtime:core", "//runtime:http", "//runtime:server", diff --git a/protocol-tests/rpcv2cbor/generated/src/client.cc b/protocol-tests/rpcv2cbor/generated/src/client.cc index 80779ed8..50b112aa 100644 --- a/protocol-tests/rpcv2cbor/generated/src/client.cc +++ b/protocol-tests/rpcv2cbor/generated/src/client.cc @@ -152,7 +152,7 @@ smithy::Outcome RpcV2ProtocolClient::Send(smithy::ht if (!request.body.empty()) { request.headers.Set("content-length", std::to_string(request.body.size())); } - return transport_->Send(request); + return smithy::SendWithRetries(*transport_, request, config_.retry); } smithy::Outcome RpcV2ProtocolClient::EmptyInputOutput(const EmptyInputOutputInput& input) const { diff --git a/protocol-tests/rpcv2cbor/generated/tests/request_tests.cc b/protocol-tests/rpcv2cbor/generated/tests/request_tests.cc index 13dc7a34..350fd706 100644 --- a/protocol-tests/rpcv2cbor/generated/tests/request_tests.cc +++ b/protocol-tests/rpcv2cbor/generated/tests/request_tests.cc @@ -28,6 +28,7 @@ struct Fixture { Fixture MakeFixture(const std::string& endpoint = "") { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; config.endpoint = endpoint; // Create cannot fail when a transport is injected. diff --git a/protocol-tests/rpcv2cbor/generated/tests/response_tests.cc b/protocol-tests/rpcv2cbor/generated/tests/response_tests.cc index bd103393..8e292cd0 100644 --- a/protocol-tests/rpcv2cbor/generated/tests/response_tests.cc +++ b/protocol-tests/rpcv2cbor/generated/tests/response_tests.cc @@ -32,6 +32,7 @@ struct Fixture { Fixture MakeFixture(const std::string& endpoint = "") { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; config.endpoint = endpoint; // Create cannot fail when a transport is injected. diff --git a/protocol-tests/rpcv2cbor/generated/tests/server_response_tests.cc b/protocol-tests/rpcv2cbor/generated/tests/server_response_tests.cc index 172d32bf..a33506ba 100644 --- a/protocol-tests/rpcv2cbor/generated/tests/server_response_tests.cc +++ b/protocol-tests/rpcv2cbor/generated/tests/server_response_tests.cc @@ -174,6 +174,7 @@ class RecordingHandler : public RpcV2ProtocolHandler { smithy::http::HttpRequest MinimalRequestForEmptyInputOutput() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); EmptyInputOutputInput input = [] { @@ -187,6 +188,7 @@ smithy::http::HttpRequest MinimalRequestForEmptyInputOutput() { smithy::http::HttpRequest MinimalRequestForNoInputOutput() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); NoInputOutputInput input = [] { @@ -200,6 +202,7 @@ smithy::http::HttpRequest MinimalRequestForNoInputOutput() { smithy::http::HttpRequest MinimalRequestForOptionalInputOutput() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); OptionalInputOutputInput input = [] { @@ -213,6 +216,7 @@ smithy::http::HttpRequest MinimalRequestForOptionalInputOutput() { smithy::http::HttpRequest MinimalRequestForRpcV2CborDenseMaps() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); RpcV2CborDenseMapsInput input = [] { @@ -226,6 +230,7 @@ smithy::http::HttpRequest MinimalRequestForRpcV2CborDenseMaps() { smithy::http::HttpRequest MinimalRequestForRpcV2CborLists() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); RpcV2CborListsInput input = [] { @@ -239,6 +244,7 @@ smithy::http::HttpRequest MinimalRequestForRpcV2CborLists() { smithy::http::HttpRequest MinimalRequestForRpcV2CborSparseMaps() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); RpcV2CborSparseMapsInput input = [] { @@ -252,6 +258,7 @@ smithy::http::HttpRequest MinimalRequestForRpcV2CborSparseMaps() { smithy::http::HttpRequest MinimalRequestForSimpleScalarProperties() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); SimpleScalarPropertiesInput input = [] { @@ -265,6 +272,7 @@ smithy::http::HttpRequest MinimalRequestForSimpleScalarProperties() { smithy::http::HttpRequest MinimalRequestForSparseNullsOperation() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); SparseNullsOperationInput input = [] { @@ -278,6 +286,7 @@ smithy::http::HttpRequest MinimalRequestForSparseNullsOperation() { smithy::http::HttpRequest MinimalRequestForGreetingWithErrors() { auto transport = std::make_shared(); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = transport; auto client = *RpcV2ProtocolClient::Create(std::move(config)); GreetingWithErrorsInput input = [] { diff --git a/protocol-tests/rpcv2cbor/generated/tests/smoke_test.cc b/protocol-tests/rpcv2cbor/generated/tests/smoke_test.cc index b911b6f3..0a278d2c 100644 --- a/protocol-tests/rpcv2cbor/generated/tests/smoke_test.cc +++ b/protocol-tests/rpcv2cbor/generated/tests/smoke_test.cc @@ -159,6 +159,7 @@ RpcV2ProtocolClient MakeClient(std::shared_ptr handler) { auto loopback = std::make_shared(); (void)loopback->Start(server.Handler()); smithy::ClientConfig config; + config.retry.max_attempts = 1; // wire-exact tests: no retries config.http_client = loopback; // Create cannot fail when a transport is injected. return *RpcV2ProtocolClient::Create(std::move(config)); diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 1c45535d..415516f5 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -171,9 +171,38 @@ cc_test( ], ) +# Gzip codec for @requestCompression bodies; separate target so consumers +# without compressed operations don't pull in zlib. +cc_library( + name = "compression", + srcs = ["src/compression/gzip.cc"], + hdrs = ["include/smithy/compression/gzip.h"], + copts = COPTS, + includes = ["include"], + deps = [ + ":core", + "@zlib", + ], +) + +cc_test( + name = "gzip_test", + size = "small", + srcs = ["tests/compression/gzip_test.cc"], + copts = COPTS, + deps = [ + ":compression", + "@googletest//:gtest_main", + ], +) + cc_library( name = "client", - hdrs = ["include/smithy/client/config.h"], + srcs = ["src/client/retry.cc"], + hdrs = [ + "include/smithy/client/config.h", + "include/smithy/client/retry.h", + ], copts = COPTS, includes = ["include"], deps = [ @@ -182,6 +211,17 @@ cc_library( ], ) +cc_test( + name = "retry_test", + size = "small", + srcs = ["tests/client/retry_test.cc"], + copts = COPTS, + deps = [ + ":client", + "@googletest//:gtest_main", + ], +) + cc_library( name = "server", srcs = ["src/server/router.cc"], diff --git a/runtime/include/smithy/client/config.h b/runtime/include/smithy/client/config.h index d88af534..34052c4b 100644 --- a/runtime/include/smithy/client/config.h +++ b/runtime/include/smithy/client/config.h @@ -4,6 +4,7 @@ #include #include +#include "smithy/client/retry.h" #include "smithy/http/transport.h" namespace smithy { @@ -21,6 +22,14 @@ struct ClientConfig { int request_timeout_ms = 30000; std::string user_agent = "smithy-cpp/0.0.0-dev"; + // Full-jitter exponential backoff for transport failures and transient + // statuses (429/5xx); retry.max_attempts = 1 disables retries. + RetryPolicy retry; + + // @requestCompression: bodies at least this large are gzip-compressed + // (the Smithy default; 0 compresses everything). + int request_min_compression_size_bytes = 10240; + // Optional transport override; shared so several clients can reuse one. std::shared_ptr http_client; }; diff --git a/runtime/include/smithy/client/retry.h b/runtime/include/smithy/client/retry.h new file mode 100644 index 00000000..33237fed --- /dev/null +++ b/runtime/include/smithy/client/retry.h @@ -0,0 +1,44 @@ +#ifndef SMITHY_CLIENT_RETRY_H_ +#define SMITHY_CLIENT_RETRY_H_ + +#include +#include + +#include "smithy/core/outcome.h" +#include "smithy/http/message.h" +#include "smithy/http/transport.h" + +namespace smithy { + +// Retry configuration for generated clients: full-jitter exponential backoff +// (retry n waits uniform(0, min(max_backoff, initial_backoff * 2^(n-1)))). +// sleep and jitter are injectable so tests run instantly and deterministically. +struct RetryPolicy { + // Total tries including the first; 1 disables retries. + int max_attempts = 3; + std::chrono::milliseconds initial_backoff{100}; + std::chrono::milliseconds max_backoff{20000}; + + // Overrides for tests; null means a real sleep / a thread-local uniform [0,1). + std::function sleep; + std::function jitter; +}; + +// The full-jitter delay before 1-based retry number `retry`. +std::chrono::milliseconds RetryDelay(const RetryPolicy& policy, int retry, double jitter01); + +// True for the HTTP statuses every Smithy SDK treats as transient: +// 429 (throttling) and 500/502/503/504. +bool RetryableStatus(int status); + +// Sends through the transport with retries: transport failures flagged +// retryable (connection, timeout) and transient response statuses are +// retried up to policy.max_attempts, sleeping the backoff in between. +// The last outcome — success or not — is returned as-is. +Outcome SendWithRetries(http::HttpClient& transport, + const http::HttpRequest& request, + const RetryPolicy& policy); + +} // namespace smithy + +#endif // SMITHY_CLIENT_RETRY_H_ diff --git a/runtime/include/smithy/compression/gzip.h b/runtime/include/smithy/compression/gzip.h new file mode 100644 index 00000000..0f2563d8 --- /dev/null +++ b/runtime/include/smithy/compression/gzip.h @@ -0,0 +1,22 @@ +#ifndef SMITHY_COMPRESSION_GZIP_H_ +#define SMITHY_COMPRESSION_GZIP_H_ + +#include +#include +#include + +#include "smithy/core/outcome.h" + +namespace smithy { + +// Gzip-compresses data (@requestCompression request bodies). +Outcome GzipCompress(std::string_view data); + +// Decompresses a gzip stream, refusing outputs larger than max_output +// (decompression-bomb guard for server-side request bodies). +Outcome GzipDecompress(std::string_view data, + std::size_t max_output = std::size_t{64} * 1024 * 1024); + +} // namespace smithy + +#endif // SMITHY_COMPRESSION_GZIP_H_ diff --git a/runtime/src/client/retry.cc b/runtime/src/client/retry.cc new file mode 100644 index 00000000..a3dce58e --- /dev/null +++ b/runtime/src/client/retry.cc @@ -0,0 +1,53 @@ +#include "smithy/client/retry.h" + +#include +#include +#include + +namespace smithy { +namespace { + +double UniformJitter() { + thread_local std::mt19937 engine{std::random_device{}()}; + return std::uniform_real_distribution(0.0, 1.0)(engine); +} + +} // namespace + +std::chrono::milliseconds RetryDelay(const RetryPolicy& policy, int retry, double jitter01) { + // Cap the exponent so the shift below cannot overflow; max_backoff clamps + // the result long before that anyway. + const int exponent = std::min(retry - 1, 20); + const auto ceiling = + std::min(policy.max_backoff, policy.initial_backoff * (std::int64_t{1} << exponent)); + return std::chrono::milliseconds( + static_cast(static_cast(ceiling.count()) * jitter01)); +} + +bool RetryableStatus(int status) { + return status == 429 || status == 500 || status == 502 || status == 503 || status == 504; +} + +Outcome SendWithRetries(http::HttpClient& transport, + const http::HttpRequest& request, + const RetryPolicy& policy) { + const auto sleep = policy.sleep != nullptr ? policy.sleep : [](std::chrono::milliseconds d) { + std::this_thread::sleep_for(d); + }; + const auto jitter = policy.jitter != nullptr ? policy.jitter : UniformJitter; + const int attempts = std::max(policy.max_attempts, 1); + + Outcome outcome = transport.Send(request); + for (int retry = 1; retry < attempts; ++retry) { + const bool retryable = + outcome.ok() ? RetryableStatus(outcome->status) : outcome.error().retryable(); + if (!retryable) { + return outcome; + } + sleep(RetryDelay(policy, retry, jitter())); + outcome = transport.Send(request); + } + return outcome; +} + +} // namespace smithy diff --git a/runtime/src/compression/gzip.cc b/runtime/src/compression/gzip.cc new file mode 100644 index 00000000..2a1914af --- /dev/null +++ b/runtime/src/compression/gzip.cc @@ -0,0 +1,72 @@ +#include "smithy/compression/gzip.h" + +#include + +#include + +namespace smithy { + +namespace { +constexpr int kGzipWindowBits = 15 + 16; // 32KB window, gzip wrapper +constexpr std::size_t kChunk = std::size_t{16} * 1024; +} // namespace + +Outcome GzipCompress(std::string_view data) { + z_stream stream{}; + if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, kGzipWindowBits, 8, + Z_DEFAULT_STRATEGY) != Z_OK) { + return Error::Serialization("gzip: deflateInit2 failed"); + } + stream.next_in = reinterpret_cast(const_cast(data.data())); + stream.avail_in = static_cast(data.size()); + + std::string out; + std::array buffer{}; + int result = Z_OK; + do { + stream.next_out = reinterpret_cast(buffer.data()); + stream.avail_out = static_cast(buffer.size()); + result = deflate(&stream, Z_FINISH); + if (result == Z_STREAM_ERROR) { + deflateEnd(&stream); + return Error::Serialization("gzip: deflate failed"); + } + out.append(buffer.data(), buffer.size() - stream.avail_out); + } while (result != Z_STREAM_END); + deflateEnd(&stream); + return out; +} + +Outcome GzipDecompress(std::string_view data, std::size_t max_output) { + z_stream stream{}; + if (inflateInit2(&stream, kGzipWindowBits) != Z_OK) { + return Error::Serialization("gzip: inflateInit2 failed"); + } + stream.next_in = reinterpret_cast(const_cast(data.data())); + stream.avail_in = static_cast(data.size()); + + std::string out; + std::array buffer{}; + int result = Z_OK; + do { + stream.next_out = reinterpret_cast(buffer.data()); + stream.avail_out = static_cast(buffer.size()); + result = inflate(&stream, Z_NO_FLUSH); + if (result != Z_OK && result != Z_STREAM_END) { + inflateEnd(&stream); + return Error::Serialization("gzip: malformed stream"); + } + out.append(buffer.data(), buffer.size() - stream.avail_out); + if (out.size() > max_output) { + inflateEnd(&stream); + return Error::Serialization("gzip: output exceeds limit"); + } + } while (result != Z_STREAM_END); + inflateEnd(&stream); + if (stream.avail_in != 0) { + return Error::Serialization("gzip: trailing garbage after stream"); + } + return out; +} + +} // namespace smithy diff --git a/runtime/tests/client/retry_test.cc b/runtime/tests/client/retry_test.cc new file mode 100644 index 00000000..b2a77d89 --- /dev/null +++ b/runtime/tests/client/retry_test.cc @@ -0,0 +1,113 @@ +#include "smithy/client/retry.h" + +#include + +#include +#include + +#include "smithy/core/error.h" + +namespace smithy { +namespace { + +using std::chrono::milliseconds; + +// Scripted transport: pops one outcome per Send. +class ScriptedTransport final : public http::HttpClient { + public: + Outcome Send(const http::HttpRequest& request) override { + (void)request; + ++calls; + if (script.empty()) return http::HttpResponse{200, {}, "fallback"}; + auto next = script.front(); + script.erase(script.begin()); + return next; + } + + std::vector> script; + int calls = 0; +}; + +RetryPolicy InstantPolicy(std::vector* slept) { + RetryPolicy policy; + policy.sleep = [slept](milliseconds d) { slept->push_back(d); }; + policy.jitter = [] { return 1.0; }; // deterministic: always the ceiling + return policy; +} + +TEST(RetryDelayTest, FullJitterExponentialWithCap) { + RetryPolicy policy; + policy.initial_backoff = milliseconds(100); + policy.max_backoff = milliseconds(350); + EXPECT_EQ(RetryDelay(policy, 1, 1.0), milliseconds(100)); + EXPECT_EQ(RetryDelay(policy, 2, 1.0), milliseconds(200)); + EXPECT_EQ(RetryDelay(policy, 3, 1.0), milliseconds(350)); // capped + EXPECT_EQ(RetryDelay(policy, 2, 0.5), milliseconds(100)); // jitter scales + EXPECT_EQ(RetryDelay(policy, 40, 1.0), milliseconds(350)); // huge retry: no overflow +} + +TEST(RetryableStatusTest, TransientStatusesOnly) { + for (int status : {429, 500, 502, 503, 504}) EXPECT_TRUE(RetryableStatus(status)) << status; + for (int status : {200, 201, 204, 400, 403, 404, 501}) { + EXPECT_FALSE(RetryableStatus(status)) << status; + } +} + +TEST(SendWithRetriesTest, RetriesTransportErrorsThenSucceeds) { + ScriptedTransport transport; + transport.script = {Error::Transport("refused"), Error::Transport("refused"), + http::HttpResponse{200, {}, "ok"}}; + std::vector slept; + const auto outcome = SendWithRetries(transport, {}, InstantPolicy(&slept)); + ASSERT_TRUE(outcome.ok()); + EXPECT_EQ(outcome->body, "ok"); + EXPECT_EQ(transport.calls, 3); + EXPECT_EQ(slept, (std::vector{milliseconds(100), milliseconds(200)})); +} + +TEST(SendWithRetriesTest, RetriesTransientStatuses) { + ScriptedTransport transport; + transport.script = {http::HttpResponse{503, {}, "busy"}, http::HttpResponse{200, {}, "ok"}}; + std::vector slept; + const auto outcome = SendWithRetries(transport, {}, InstantPolicy(&slept)); + ASSERT_TRUE(outcome.ok()); + EXPECT_EQ(outcome->status, 200); + EXPECT_EQ(transport.calls, 2); +} + +TEST(SendWithRetriesTest, DoesNotRetryClientErrors) { + ScriptedTransport transport; + transport.script = {http::HttpResponse{404, {}, "nope"}}; + std::vector slept; + const auto outcome = SendWithRetries(transport, {}, InstantPolicy(&slept)); + ASSERT_TRUE(outcome.ok()); + EXPECT_EQ(outcome->status, 404); + EXPECT_EQ(transport.calls, 1); + EXPECT_TRUE(slept.empty()); +} + +TEST(SendWithRetriesTest, GivesUpAfterMaxAttempts) { + ScriptedTransport transport; + transport.script = {Error::Transport("a"), Error::Transport("b"), Error::Transport("c"), + Error::Transport("d")}; + std::vector slept; + const auto outcome = SendWithRetries(transport, {}, InstantPolicy(&slept)); + ASSERT_FALSE(outcome.ok()); + EXPECT_EQ(outcome.error().message(), "c"); // the third (last) attempt's failure + EXPECT_EQ(transport.calls, 3); +} + +TEST(SendWithRetriesTest, MaxAttemptsOneDisablesRetries) { + ScriptedTransport transport; + transport.script = {http::HttpResponse{503, {}, "busy"}}; + std::vector slept; + RetryPolicy policy = InstantPolicy(&slept); + policy.max_attempts = 1; + const auto outcome = SendWithRetries(transport, {}, policy); + ASSERT_TRUE(outcome.ok()); + EXPECT_EQ(outcome->status, 503); + EXPECT_EQ(transport.calls, 1); +} + +} // namespace +} // namespace smithy diff --git a/runtime/tests/compression/gzip_test.cc b/runtime/tests/compression/gzip_test.cc new file mode 100644 index 00000000..ddf5db9d --- /dev/null +++ b/runtime/tests/compression/gzip_test.cc @@ -0,0 +1,47 @@ +#include "smithy/compression/gzip.h" + +#include + +#include + +namespace smithy { +namespace { + +TEST(GzipTest, RoundTrips) { + const std::string text(100000, 'a'); + const auto compressed = GzipCompress(text); + ASSERT_TRUE(compressed.ok()); + EXPECT_LT(compressed->size(), text.size() / 10); // trivially compressible + const auto restored = GzipDecompress(*compressed); + ASSERT_TRUE(restored.ok()); + EXPECT_EQ(*restored, text); +} + +TEST(GzipTest, RoundTripsEmpty) { + const auto compressed = GzipCompress(""); + ASSERT_TRUE(compressed.ok()); + const auto restored = GzipDecompress(*compressed); + ASSERT_TRUE(restored.ok()); + EXPECT_EQ(*restored, ""); +} + +TEST(GzipTest, RejectsGarbage) { + EXPECT_FALSE(GzipDecompress("definitely not gzip").ok()); + EXPECT_FALSE(GzipDecompress("").ok()); +} + +TEST(GzipTest, EnforcesOutputLimit) { + const std::string text(100000, 'a'); + const auto compressed = GzipCompress(text); + ASSERT_TRUE(compressed.ok()); + EXPECT_FALSE(GzipDecompress(*compressed, 1024).ok()); +} + +TEST(GzipTest, RejectsTrailingGarbage) { + const auto compressed = GzipCompress("payload"); + ASSERT_TRUE(compressed.ok()); + EXPECT_FALSE(GzipDecompress(*compressed + "extra").ok()); +} + +} // namespace +} // namespace smithy