github ministackorg/ministack v1.4.18

7 hours ago

What's Changed

New Contributors

[1.4.18] — 2026-08-16

Added

  • SES v2 — email template CRUD and SendEmail with Content.Template — the v2 SendEmail handler read only Content.Simple and Content.Raw, so a templated send was accepted with a 200 and a MessageId while the template was silently dropped, delivering an empty subject and body. Content.Template is now rendered through the same {{placeholder}} substitution v1 uses, from a stored template or from TemplateContent supplied inline, and a named template that doesn't exist fails the send with NotFoundException instead of delivering an empty email. CreateEmailTemplate, GetEmailTemplate, UpdateEmailTemplate, DeleteEmailTemplate, and ListEmailTemplates are served at /v2/email/templates[/{TemplateName}] over the shared v1 store, so a template created with aws ses create-template is sendable through aws sesv2 send-email. ListEmailTemplates paginates: PageSize accepts the documented 1–100 and defaults to 10, with an opaque NextToken returned only while further templates remain. Contributed by @mikolajk0wal.
  • IoT Core — fleet indexingSearchIndex (POST /indices/search) and the indexing-configuration operations (UpdateIndexingConfiguration / GetIndexingConfiguration / DescribeIndex / ListIndices) were unimplemented. The AWS_Things index is OFF by default; enabling it with UpdateIndexingConfiguration (as Terraform's aws_iot_indexing_configuration does) lets SearchIndex answer AND-separated thingName / thingTypeName / thingGroupNames / attributes.* / shadow.desired|reported.* terms — */? wildcards, numeric shadow compare, maxResults/nextToken paging with the documented ceiling of 100 — queried directly against the live registry and classic shadows. Searching while thingIndexingMode is OFF returns ResourceNotFoundException, and REGISTRY vs REGISTRY_AND_SHADOW gates the shadow terms. Contributed by @iot-rocket.
  • IoT Core — RegisterCertificateWithoutCA and the deprecated principal-policy operationsRegisterCertificateWithoutCA (POST /certificate/register-no-ca) shares the RegisterCertificate store, returning ResourceAlreadyExistsException with resourceId/resourceArn on a duplicate PEM. The deprecated AttachPrincipalPolicy / DetachPrincipalPolicy / ListPrincipalPolicies / ListPolicyPrincipals map onto the modern policy-target store, with the principal in the x-amzn-iot-principal header and the policy name in x-amzn-iot-policy. Contributed by @iot-rocket.
  • RDS — Aurora MySQL compatibility objects — Aurora MySQL provisioning that creates users with AWSAuthenticationPlugin and calls RDS-specific stored procedures failed against upstream MySQL images. MiniStack now builds ABI-matched MySQL 8.0/8.4 AWSAuthenticationPlugin artifacts (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 with MINISTACK_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: gzip and Content-Encoding: aws-chunked as separate lines: MiniStack saw only aws-chunked, stripped it as the chunk-framing marker it is, and stored the object with no content encoding at all — silently losing gzip for every Java SDK v2 caller, S3Proxy's aws-s3 backend among them. Repeated field lines now combine into one comma-joined value as RFC 9110 5.2 requires (Cookie rejoins with "; " per RFC 9113 8.2.3), so aws-chunked is 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 reported An error occurred (404) rather than NotFoundException, and typed handling never matched on any SESv2 operation. The 1.3.24 sweep that added the header centrally in error_response_json reached ses but not ses_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 outputAWS and AWS_PROXY shared one response path, so a custom-integration handler returning a plain document had it read as a {statusCode, headers, body} proxy envelope: a statusCode key 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 no selectionPattern is configured), and an uninvokable or throttled backend is a 504 integration failure. AWS_PROXY is unchanged. Contributed by @iot-rocket.
  • IoT — topic-rule WHERE evaluation, SQL functions, and non-Lambda actions — rules ignored the WHERE clause, so every publish matching the topic filter dispatched, and Lambda was the only action wired up. The rules engine now evaluates WHERE with AWS's three-valued (Undefined) logic — =/<>/</>/BETWEEN/IN/LIKE/IS NULL/regexp_matches, AND/OR/NOT, and arithmetic — implements the clientid / encode / isundefined / newuuid / regexp_matches / replace / timestamp / topic functions (an unimplemented function resolves to Undefined and warns once), and dispatches the republish, dynamoDBv2, and sns actions, running the rule's errorAction on 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 Allow for GET /alpha also authorized GET /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 (missing policyDocument or principalId) answers 500 and 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 Allow leaked across routes. It now caches the output (the policy document for an IAM-policy response, the isAuthorized boolean 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 enginesAWS_ENDPOINT_URL inside a Lambda container is rewritten to host.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 map host.docker.internal to host-gateway (as the ECS and EKS paths already do); an explicit --add-host still wins. Contributed by @iot-rocket.
  • IoT — MQTT UNSUBSCRIBE removes 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 preserved cleanSession=0 session 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 Identifiersign_leaf_certificate omitted it, and RFC 5280 requires it on every certificate that is not self-issued. Python 3.13 enables ssl.VERIFY_X509_STRICT in ssl.create_default_context(), so any client on a current Python rejected what MiniStack minted with "Missing Authority Key Identifier" — CreateKeysAndCertificate certificates 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 failed INIT (RIE answers 502, raised as HTTPError) and a read timeout (bare TimeoutError). A function whose module fails to import re-ran INIT ~10×/second up to its Timeout — a 900-second function pinned the emulator ~15 minutes — and the caller got a generic error instead of the real Runtime.ImportModuleError. Both are now terminal: an init failure returns 200 with X-Amz-Function-Error: Unhandled and the runtime's own error payload, a timeout answers Runtime.ExitError and recycles the container, and only genuine connection failures still retry within a bounded cold-start window. Contributed by @iot-rocket.
  • Lambda — RecursiveLoop is enforcedPutFunctionRecursionConfig stored the setting and nothing read it, so a self-invoking function looped unchecked (an async self-invoke returns 202, 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 with RecursiveInvocationException and a RecursiveInvocationsDropped metric, unless the function's RecursiveLoop is Allow. 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 rejected 42P17: functions in index expression must be marked IMMUTABLE, an expression in an INCLUDE column 0A000) and aligns the surrounding errors with a live cluster: plain CREATE INDEX is always refused (unsupported mode. please use CREATE INDEX ASYNC.), USING / CONCURRENTLY / WHERE return DSQL's own messages, 9+ key columns 54011, a nameless IF NOT EXISTS 42601, and a primary-key column drop reports cannot 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-Length or 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 headersIf-Match, If-None-Match, If-Modified-Since and If-Unmodified-Since were honoured on PutObject, CopyObject and DeleteObject but ignored on the reads, so every conditional read returned 200 and 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 answer 412 PreconditionFailed or 304 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 a 412 preempts a Range rather than slicing a representation the caller rejected. The 304 keeps 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.

Don't miss a new ministack release

NewReleases is sending notifications on new releases.