What's Changed
New Contributors
- @mikolajk0wal made their first contribution in #1357
- @gaul made their first contribution in #1388
- @uttom-akash made their first contribution in #1393
[1.4.18] — 2026-08-16
Added
- SES v2 — email template CRUD and
SendEmailwithContent.Template— the v2SendEmailhandler read onlyContent.SimpleandContent.Raw, so a templated send was accepted with a200and aMessageIdwhile the template was silently dropped, delivering an empty subject and body.Content.Templateis now rendered through the same{{placeholder}}substitution v1 uses, from a stored template or fromTemplateContentsupplied inline, and a named template that doesn't exist fails the send withNotFoundExceptioninstead of delivering an empty email.CreateEmailTemplate,GetEmailTemplate,UpdateEmailTemplate,DeleteEmailTemplate, andListEmailTemplatesare served at/v2/email/templates[/{TemplateName}]over the shared v1 store, so a template created withaws ses create-templateis sendable throughaws sesv2 send-email.ListEmailTemplatespaginates:PageSizeaccepts the documented 1–100 and defaults to 10, with an opaqueNextTokenreturned only while further templates remain. Contributed by @mikolajk0wal. - IoT Core — fleet indexing —
SearchIndex(POST /indices/search) and the indexing-configuration operations (UpdateIndexingConfiguration/GetIndexingConfiguration/DescribeIndex/ListIndices) were unimplemented. TheAWS_Thingsindex isOFFby default; enabling it withUpdateIndexingConfiguration(as Terraform'saws_iot_indexing_configurationdoes) letsSearchIndexanswerAND-separatedthingName/thingTypeName/thingGroupNames/attributes.*/shadow.desired|reported.*terms —*/?wildcards, numeric shadow compare,maxResults/nextTokenpaging with the documented ceiling of 100 — queried directly against the live registry and classic shadows. Searching whilethingIndexingModeisOFFreturnsResourceNotFoundException, andREGISTRYvsREGISTRY_AND_SHADOWgates the shadow terms. Contributed by @iot-rocket. - IoT Core —
RegisterCertificateWithoutCAand the deprecated principal-policy operations —RegisterCertificateWithoutCA(POST /certificate/register-no-ca) shares theRegisterCertificatestore, returningResourceAlreadyExistsExceptionwithresourceId/resourceArnon a duplicate PEM. The deprecatedAttachPrincipalPolicy/DetachPrincipalPolicy/ListPrincipalPolicies/ListPolicyPrincipalsmap onto the modern policy-target store, with the principal in thex-amzn-iot-principalheader and the policy name inx-amzn-iot-policy. Contributed by @iot-rocket. - RDS — Aurora MySQL compatibility objects — Aurora MySQL provisioning that creates users with
AWSAuthenticationPluginand calls RDS-specific stored procedures failed against upstream MySQL images. MiniStack now builds ABI-matched MySQL 8.0/8.4AWSAuthenticationPluginartifacts (amd64/arm64), installs a reject-all compatibility plugin without changing password authentication, and adds the Aurora/RDS procedures, configuration surface, and predefined S3 roles those workflows expect. The path is automatic for supported MySQL images and can be disabled withMINISTACK_MYSQL_IAM_AUTH=off; the plugin deliberately rejects authentication, so the existing non-enforcement stance is unchanged. Contributed by @Areson.
Fixed
- Request headers — a field repeated across lines is no longer reduced to its last line — the ASGI header dict was built with plain assignment, so when a client sent the same field twice the earlier line was discarded. The AWS SDK for Java v2 uploads exactly that way, emitting
Content-Encoding: gzipandContent-Encoding: aws-chunkedas separate lines: MiniStack saw onlyaws-chunked, stripped it as the chunk-framing marker it is, and stored the object with no content encoding at all — silently losinggzipfor every Java SDK v2 caller, S3Proxy'saws-s3backend among them. Repeated field lines now combine into one comma-joined value as RFC 9110 5.2 requires (Cookierejoins with"; "per RFC 9113 8.2.3), soaws-chunkedis stripped from the joined list and the caller's encoding survives. Sending the header once, in either order, was already correct and is unchanged. Contributed by @gaul. - SES v2 — error responses carry
x-amzn-errortype— restJson1 resolves the error shape from that header, so without it SDKs surfaced a bare HTTP status instead of the modelled exception: boto3 reportedAn error occurred (404)rather thanNotFoundException, and typed handling never matched on any SESv2 operation. The 1.3.24 sweep that added the header centrally inerror_response_jsonreachedsesbut notses_v2, which builds its error bodies itself; it is now set on all of them. Contributed by @mikolajk0wal. - API Gateway — non-proxy (
AWS) Lambda integrations return the handler's raw output —AWSandAWS_PROXYshared one response path, so a custom-integration handler returning a plain document had it read as a{statusCode, headers, body}proxy envelope: astatusCodekey in the data became the HTTP status and the rest was dropped. A non-proxy integration now serializes the return value as the response body with the integration response's status (200 by default); a standard Lambda error is passed through as the body at the default 200 status (as AWS does when noselectionPatternis configured), and an uninvokable or throttled backend is a504integration failure.AWS_PROXYis unchanged. Contributed by @iot-rocket. - IoT — topic-rule
WHEREevaluation, SQL functions, and non-Lambda actions — rules ignored theWHEREclause, so every publish matching the topic filter dispatched, and Lambda was the only action wired up. The rules engine now evaluatesWHEREwith AWS's three-valued (Undefined) logic —=/<>/</>/BETWEEN/IN/LIKE/IS NULL/regexp_matches,AND/OR/NOT, and arithmetic — implements theclientid/encode/isundefined/newuuid/regexp_matches/replace/timestamp/topicfunctions (an unimplemented function resolves to Undefined and warns once), and dispatches therepublish,dynamoDBv2, andsnsactions, running the rule'serrorActionon a delivery failure. Contributed by @iot-rocket. - API Gateway (REST / v1) — Lambda authorizer cache is scoped to the method ARN and stage — the cache stored the allow/deny decision keyed on the identity source alone, so a cached
AllowforGET /alphaalso authorizedGET /beta, and a verdict cached on one stage was served for another. It now caches the authorizer's policy document and re-evaluates it against each request's own method ARN, with the stage and API in the key; malformed authorizer output (missingpolicyDocumentorprincipalId) answers500and is never cached. Contributed by @iot-rocket. - API Gateway (HTTP API / v2) — REQUEST-authorizer cache is scoped to the route ARN — the v2 cache stored the verdict rather than the authorizer's response, so a cached
Allowleaked across routes. It now caches the output (the policy document for an IAM-policy response, theisAuthorizedboolean for a simple response) and re-evaluates the policy against each request's own route ARN, keyed on the identity source values and stage; caching requires at least one identity source and the cache is bounded. Contributed by @iot-rocket. - Lambda — a Docker-executor handler's callbacks resolve on native Linux engines —
AWS_ENDPOINT_URLinside a Lambda container is rewritten tohost.docker.internal, which only resolves on Docker Desktop, so on a native Linux engine every nested SDK call a handler made died on DNS. Lambda containers now maphost.docker.internaltohost-gateway(as the ECS and EKS paths already do); an explicit--add-hoststill wins. Contributed by @iot-rocket. - IoT — MQTT
UNSUBSCRIBEremoves only the topic filters it names — the MQTT-over-WebSocket bridge never read the filter list in the packet and unsubscribed every subscription the session held, so a client unsubscribing from one topic stopped receiving all the others. It now removes only the named filters (matched character-by-character per MQTT 3.1.1 §3.10.4, so a wildcard matches its own text), popping the id from every per-session map so a preservedcleanSession=0session cannot resurrect a removed filter, and a truncated payload unsubscribes what parsed rather than tearing the session down. Contributed by @iot-rocket. - TLS — minted leaf certificates carry an Authority Key Identifier —
sign_leaf_certificateomitted it, and RFC 5280 requires it on every certificate that is not self-issued. Python 3.13 enablesssl.VERIFY_X509_STRICTinssl.create_default_context(), so any client on a current Python rejected what MiniStack minted with "Missing Authority Key Identifier" —CreateKeysAndCertificatecertificates included. Contributed by @iot-rocket. - Lambda (Docker executor) — a failed init or a timed-out handler is reported once, not retried — the RIE invoke loop's single connection-retry arm swallowed two terminal cases that both look like
OSError: a failedINIT(RIE answers502, raised asHTTPError) and a read timeout (bareTimeoutError). A function whose module fails to import re-ranINIT~10×/second up to itsTimeout— a 900-second function pinned the emulator ~15 minutes — and the caller got a generic error instead of the realRuntime.ImportModuleError. Both are now terminal: an init failure returns200withX-Amz-Function-Error: Unhandledand the runtime's own error payload, a timeout answersRuntime.ExitErrorand recycles the container, and only genuine connection failures still retry within a bounded cold-start window. Contributed by @iot-rocket. - Lambda —
RecursiveLoopis enforced —PutFunctionRecursionConfigstored the setting and nothing read it, so a self-invoking function looped unchecked (an async self-invoke returns202, releases its worker, and never throttles). A per-request lineage depth now travels with direct Lambda → Lambda invocations; past ~16 hops the invocation is dropped withRecursiveInvocationExceptionand aRecursiveInvocationsDroppedmetric, unless the function'sRecursiveLoopisAllow. Loops that leave through SQS, SNS or S3 and back are not yet detected. Contributed by @iot-rocket. - Aurora DSQL — index and DDL error parity, including expression index keys — the wire proxy now allows expression index keys on
CREATE INDEX ASYNC(a volatile function is rejected42P17: functions in index expression must be marked IMMUTABLE, an expression in anINCLUDEcolumn0A000) and aligns the surrounding errors with a live cluster: plainCREATE INDEXis always refused (unsupported mode. please use CREATE INDEX ASYNC.),USING/CONCURRENTLY/WHEREreturn DSQL's own messages, 9+ key columns54011, a namelessIF NOT EXISTS42601, and a primary-key column drop reportscannot drop primary key column <name>. Contributed by @ry-allan. - ALB — target responses are streamed instead of buffered — the data plane read a target's entire response before returning, so a streaming or chunked target reached the client only once it had finished producing (measured time-to-first-byte for a 700 KB stream: 11.28s → 0.004s). It now relays the body as the target produces it, keeping the target's framing (
Content-Lengthor chunked) and bounding an established connection by an idle timeout (default 60s) distinct from the 10s connect deadline; a target that dies mid-body surfaces as a truncated response rather than a clean one. Contributed by @uttom-akash. - S3 — GetObject and HeadObject answer the conditional-read headers —
If-Match,If-None-Match,If-Modified-SinceandIf-Unmodified-Sincewere honoured on PutObject, CopyObject and DeleteObject but ignored on the reads, so every conditional read returned200and the whole body: a cache revalidating an unchanged object refetched it in full, and a read guarded against a concurrent overwrite never noticed one. Reads now answer412 PreconditionFailedor304 Not Modified, with the precedence RFC 9110 13.2.2 and the AWS GetObject reference define — an entity tag decides, and its date counterpart applies only in its absence — and a412preempts aRangerather than slicing a representation the caller rejected. The304keeps the validators (ETag,Last-Modified) and drops the headers describing a payload it cannot carry,x-amz-checksum-*included, which boto3 asks for by default and would otherwise try to validate against the empty body. Reported by @gaul.