286 lines
13 KiB
Markdown
286 lines
13 KiB
Markdown
# SNS
|
|
|
|
**Protocol:** Query (XML) and JSON 1.0 (both supported)
|
|
**Endpoint:** `POST http://localhost:4566/`
|
|
|
|
## Supported Actions
|
|
|
|
| Action | Description |
|
|
|---|---|
|
|
| `CreateTopic` | Create a topic |
|
|
| `DeleteTopic` | Delete a topic |
|
|
| `ListTopics` | List all topics |
|
|
| `GetTopicAttributes` | Get topic configuration |
|
|
| `SetTopicAttributes` | Update topic configuration |
|
|
| `Subscribe` | Subscribe an endpoint (SQS, HTTP, Lambda, email) |
|
|
| `Unsubscribe` | Remove a subscription |
|
|
| `ListSubscriptions` | List all subscriptions |
|
|
| `ListSubscriptionsByTopic` | List subscriptions for a specific topic |
|
|
| `GetSubscriptionAttributes` | Get subscription settings |
|
|
| `SetSubscriptionAttributes` | Update subscription settings |
|
|
| `ConfirmSubscription` | Confirm a pending subscription |
|
|
| `Publish` | Publish a message to a topic |
|
|
| `PublishBatch` | Publish up to 10 messages in one call |
|
|
| `TagResource` | Tag a topic |
|
|
| `UntagResource` | Remove tags from a topic |
|
|
| `ListTagsForResource` | List tags on a topic |
|
|
| `CreatePlatformApplication` | Create a mobile push platform app (iOS or Android) |
|
|
| `DeletePlatformApplication` | Delete a platform app and its endpoints |
|
|
| `GetPlatformApplicationAttributes` | Read platform app attributes |
|
|
| `SetPlatformApplicationAttributes` | Update platform app attributes (e.g. `Enabled`) |
|
|
| `ListPlatformApplications` | List platform applications in the region |
|
|
| `CreatePlatformEndpoint` | Register a device token under a platform app |
|
|
| `DeleteEndpoint` | Delete a platform endpoint |
|
|
| `GetEndpointAttributes` | Read endpoint attributes |
|
|
| `SetEndpointAttributes` | Update endpoint attributes (e.g. `Enabled=false` to simulate token expiry) |
|
|
| `ListEndpointsByPlatformApplication` | List endpoints under a platform app |
|
|
|
|
## Configuration
|
|
|
|
| Variable | Default | Description |
|
|
|---|---|---|
|
|
| `FLOCI_SERVICES_SNS_ENABLED` | `true` | Enable or disable the service |
|
|
| `FLOCI_STORAGE_SERVICES_SNS_MODE` | *(global default)* | Storage mode override for SNS (`memory`, `persistent`, `hybrid`, `wal`) |
|
|
| `FLOCI_STORAGE_SERVICES_SNS_FLUSH_INTERVAL_MS` | `5000` | Flush interval for `hybrid`/`wal` storage modes (milliseconds) |
|
|
|
|
## Examples
|
|
|
|
```bash
|
|
export AWS_ENDPOINT_URL=http://localhost:4566
|
|
|
|
# Create a topic
|
|
TOPIC_ARN=$(aws sns create-topic --name notifications \
|
|
--query TopicArn --output text \
|
|
--endpoint-url $AWS_ENDPOINT_URL)
|
|
|
|
# Subscribe an SQS queue
|
|
QUEUE_ARN=$(aws sqs get-queue-attributes \
|
|
--queue-url $AWS_ENDPOINT_URL/000000000000/orders \
|
|
--attribute-names QueueArn \
|
|
--query Attributes.QueueArn --output text \
|
|
--endpoint-url $AWS_ENDPOINT_URL)
|
|
|
|
aws sns subscribe \
|
|
--topic-arn $TOPIC_ARN \
|
|
--protocol sqs \
|
|
--notification-endpoint $QUEUE_ARN \
|
|
--endpoint-url $AWS_ENDPOINT_URL
|
|
|
|
# Publish a message
|
|
aws sns publish \
|
|
--topic-arn $TOPIC_ARN \
|
|
--message '{"event":"user.registered"}' \
|
|
--endpoint-url $AWS_ENDPOINT_URL
|
|
|
|
# Fan-out: publish and verify the SQS queue received the message
|
|
aws sqs receive-message \
|
|
--queue-url $AWS_ENDPOINT_URL/000000000000/orders \
|
|
--endpoint-url $AWS_ENDPOINT_URL
|
|
```
|
|
|
|
## SNS → SQS Fan-Out
|
|
|
|
Floci supports real SNS → SQS fan-out. When you publish to a topic, all SQS-subscribed queues receive the message immediately.
|
|
|
|
Supported subscription protocols:
|
|
- `sqs`: delivers to a Floci SQS queue
|
|
- `lambda`: invokes a Floci Lambda function
|
|
- `firehose`: puts records to a Floci Firehose delivery stream
|
|
- `http` / `https`: posts to an HTTP endpoint
|
|
- `application`: fans out to a mobile push platform endpoint (see [Mobile push](#mobile-push-mock))
|
|
|
|
## Message size
|
|
|
|
`MaximumMessageSize` is the per-topic limit, in bytes, on a published payload. It accepts `1024`
|
|
to `1048576` (1 MiB) and defaults to `262144` (256 KiB). AWS raised the maximum in September 2026
|
|
but left the default alone, so a topic that never sets the attribute behaves exactly as it did
|
|
before. `GetTopicAttributes` omits the attribute until it is set rather than reporting the
|
|
default.
|
|
|
|
```bash
|
|
aws sns set-topic-attributes --topic-arn $TOPIC_ARN \
|
|
--attribute-name MaximumMessageSize --attribute-value 1048576 \
|
|
--endpoint-url $AWS_ENDPOINT_URL
|
|
```
|
|
|
|
The limit counts the message body plus, per message attribute, its name, data type and value.
|
|
`Subject` is not counted. `PublishBatch` counts the sum of all its entries against the same
|
|
limit and fails the whole call rather than individual entries.
|
|
|
|
A topic above `262144` is restricted: at most 100 subscriptions, every one of them `sqs`,
|
|
`firehose` or `lambda`. Floci enforces the protocol rule on `Subscribe` and `SetTopicAttributes`
|
|
alike, and the subscription count on `SetTopicAttributes` only — matching AWS, which lets an
|
|
already-raised topic drift past 100 and catches it the next time the attribute is set. Pending
|
|
confirmations count towards both.
|
|
|
|
| Action | Condition | Error code | HTTP |
|
|
|---|---|---|---|
|
|
| `CreateTopic`, `SetTopicAttributes` | `MaximumMessageSize` not an integer between 1024 and 1048576 | `InvalidParameter` | 400 |
|
|
| `SetTopicAttributes` | Raised above 262144 with a subscription that is not `sqs`, `firehose` or `lambda` | `InvalidParameter` | 400 |
|
|
| `SetTopicAttributes` | Raised above 262144 with more than 100 subscriptions | `InvalidParameter` | 400 |
|
|
| `Subscribe` | Unsupported protocol on a topic above 262144 | `InvalidParameter` | 400 |
|
|
| `Publish` | Payload exceeds the topic's `MaximumMessageSize` | `InvalidParameter` | 400 |
|
|
| `PublishBatch` | Entries sum to more than the topic's `MaximumMessageSize` | `BatchRequestTooLong` | 400 |
|
|
|
|
CloudFormation carries the setting through: `AWS::SNS::Topic` forwards `MaximumMessageSize`, and
|
|
an update that drops the property returns the topic to the default.
|
|
|
|
A raised topic fanning out to SQS needs headroom. The notification envelope wraps the body in a
|
|
few hundred bytes of JSON, so a publish at 1 MiB no longer fits a queue at the SQS maximum and is
|
|
dropped on delivery — silently, as any delivery failure is. Keep the topic below the queue's own
|
|
`MaximumMessageSize`, or subscribe with `RawMessageDelivery=true` so the body is forwarded
|
|
unwrapped.
|
|
|
|
## FIFO topics
|
|
|
|
A topic whose name ends in `.fifo` is a FIFO topic. `Publish` and `PublishBatch` require a
|
|
`MessageGroupId`, and a message is deduplicated against its `MessageDeduplicationId` for five
|
|
minutes. Set `ContentBasedDeduplication` on the topic to derive that id from the message body.
|
|
|
|
The `FifoThroughputScope` attribute decides how wide that deduplication reaches:
|
|
|
|
| Value | Deduplication scope |
|
|
|---|---|
|
|
| `Topic` (default) | Across the whole topic: the same `MessageDeduplicationId` is a duplicate no matter which message group it arrives under |
|
|
| `MessageGroup` | Within a single message group: the same `MessageDeduplicationId` under two different `MessageGroupId`s is two distinct messages |
|
|
|
|
`MessageGroup` is what a fan-out that reuses one deduplication id per group needs, for example
|
|
publishing the same event to a topic once per tenant with the tenant as the message group.
|
|
|
|
```bash
|
|
aws sns create-topic --name events.fifo \
|
|
--attributes FifoTopic=true,FifoThroughputScope=MessageGroup \
|
|
--endpoint-url $AWS_ENDPOINT_URL
|
|
```
|
|
|
|
When the topic forwards to an SQS FIFO queue, set the matching queue attributes
|
|
(`DeduplicationScope=messageGroup` and `FifoThroughputLimit=perMessageGroupId`), otherwise the
|
|
queue deduplicates topic-wide on the way in.
|
|
|
|
CloudFormation carries both settings through: `AWS::SNS::Topic` forwards `FifoThroughputScope` and
|
|
`ContentBasedDeduplication`, and a FIFO topic left unnamed gets a generated name ending in `.fifo`.
|
|
|
|
## Control Tower managed topic
|
|
|
|
AWS Control Tower creates the regional
|
|
`aws-controltower-AggregateSecurityNotifications` topic before Landing Zone
|
|
Accelerator deploys its audit notification forwarder. Floci lazily materializes that
|
|
exact same-account managed topic when `Subscribe` first references it, matching the
|
|
Control Tower prerequisite without weakening normal SNS validation. Subscribing to
|
|
any other missing topic still returns `NotFound`.
|
|
|
|
## Mobile push (mock)
|
|
|
|
Floci mocks SNS mobile push for iOS and Android. No real APNS or FCM connection is
|
|
made — every push is captured in memory so tests can assert what would have been sent.
|
|
|
|
**Supported platforms:** `APNS`, `APNS_SANDBOX`, `GCM`, `FCM`. Any other platform
|
|
value returns `InvalidParameter`.
|
|
|
|
### End-to-end flow
|
|
|
|
```bash
|
|
APP_ARN=$(aws sns create-platform-application \
|
|
--name ios-app --platform APNS \
|
|
--attributes PlatformCredential=fake-cert \
|
|
--endpoint-url http://localhost:4566 --query PlatformApplicationArn --output text)
|
|
|
|
ENDPOINT_ARN=$(aws sns create-platform-endpoint \
|
|
--platform-application-arn $APP_ARN \
|
|
--token ios-device-token-abc \
|
|
--endpoint-url http://localhost:4566 --query EndpointArn --output text)
|
|
|
|
# Plain string payload
|
|
aws sns publish --target-arn $ENDPOINT_ARN --message '{"aps":{"alert":"hi"}}' \
|
|
--endpoint-url http://localhost:4566
|
|
|
|
# Platform-specific payloads with MessageStructure=json
|
|
aws sns publish --target-arn $ENDPOINT_ARN --message-structure json \
|
|
--message '{"default":"fallback","APNS":"{\"aps\":{\"alert\":\"ios\"}}","GCM":"{\"notification\":{\"body\":\"android\"}}"}' \
|
|
--endpoint-url http://localhost:4566
|
|
```
|
|
|
|
When `MessageStructure="json"`, Floci picks the key matching the endpoint's platform
|
|
(`APNS`, `APNS_SANDBOX`, `GCM`, or `FCM`), falling back to `default`. The envelope
|
|
must be a JSON object and must include `default` — otherwise `InvalidParameter`.
|
|
|
|
### Broadcast to devices via a topic
|
|
|
|
Subscribe platform endpoints to a topic with `Protocol="application"`, then publish to
|
|
the topic to fan out to every subscribed device — each endpoint is captured exactly as
|
|
if you had published to it directly (same platform-payload resolution, same `Enabled`
|
|
gating). A disabled endpoint in the fan-out is skipped; the rest still receive the push.
|
|
|
|
```bash
|
|
aws sns subscribe --topic-arn $TOPIC_ARN \
|
|
--protocol application --notification-endpoint $ENDPOINT_ARN \
|
|
--endpoint-url http://localhost:4566
|
|
|
|
aws sns publish --topic-arn $TOPIC_ARN --message-structure json \
|
|
--message '{"default":"market open","GCM":"{\"notification\":{\"body\":\"market alert\"}}"}' \
|
|
--endpoint-url http://localhost:4566
|
|
```
|
|
|
|
Broadcast pushes surface in the same retrospection API, keyed by `EndpointArn`.
|
|
|
|
### Per-protocol payloads on topic publish
|
|
|
|
`MessageStructure="json"` resolves per subscriber, not just for mobile endpoints. Each
|
|
subscription receives the value under its own protocol key — `sqs`, `lambda`, `http`,
|
|
`https`, `email`, `email-json`, `sms`, or the push platform (`APNS`, `GCM`, …) for
|
|
`application` — falling back to `default` when that key is absent.
|
|
|
|
```bash
|
|
aws sns publish --topic-arn $TOPIC_ARN --message-structure json \
|
|
--message '{"default":"hello","sqs":"hi sqs","GCM":"{\"notification\":{\"body\":\"hi device\"}}"}' \
|
|
--endpoint-url http://localhost:4566
|
|
```
|
|
|
|
The SQS subscriber receives `hi sqs`, the platform endpoint receives the `GCM` payload,
|
|
and every other subscriber receives `hello`. As with any topic publish, the envelope must
|
|
be a JSON object carrying `default`, validated before fan-out begins.
|
|
|
|
### Inspecting captured pushes
|
|
|
|
```bash
|
|
# All captured pushes (newest first), or filtered by endpoint
|
|
curl http://localhost:4566/_aws/sns/push-notifications
|
|
curl "http://localhost:4566/_aws/sns/push-notifications?EndpointArn=$ENDPOINT_ARN"
|
|
|
|
# Reset between tests
|
|
curl -X DELETE http://localhost:4566/_aws/sns/push-notifications
|
|
```
|
|
|
|
### Simulating expired tokens
|
|
|
|
Two ways to make `Publish` fail with `EndpointDisabledException`:
|
|
|
|
1. **Explicit** — call `SetEndpointAttributes` with `Enabled=false`. Matches the
|
|
real AWS flow after an async APNS/FCM failure.
|
|
2. **Sentinel** — create an endpoint whose token contains `EXPIRED`
|
|
(case-insensitive). Floci marks it `Enabled=false` on creation, so the first
|
|
publish fails. Lets you exercise the unhappy path with a single API call.
|
|
|
|
### Error codes
|
|
|
|
| Action | Condition | Error code | HTTP |
|
|
|---|---|---|---|
|
|
| `CreatePlatformApplication` | Missing `Name` | `InvalidParameter` | 400 |
|
|
| `CreatePlatformApplication` | Unsupported `Platform` (e.g. `WNS`, `ADM`) | `InvalidParameter` | 400 |
|
|
| `CreatePlatformEndpoint` | Missing `Token` | `InvalidParameter` | 400 |
|
|
| `CreatePlatformEndpoint` | Unknown `PlatformApplicationArn` | `NotFound` | 404 |
|
|
| `CreatePlatformEndpoint` | Same `Token`, different `CustomUserData` or attrs | `InvalidParameter` | 400 |
|
|
| `CreatePlatformEndpoint` | Platform app disabled | `PlatformApplicationDisabledException` | 400 |
|
|
| `Publish` | Unknown endpoint ARN | `NotFound` | 404 |
|
|
| `Publish` | `TargetArn` is a platform application ARN | `InvalidParameter` | 400 |
|
|
| `Publish` | Endpoint `Enabled=false` | `EndpointDisabledException` | 400 |
|
|
| `Publish` | Platform application `Enabled=false` | `PlatformApplicationDisabledException` | 400 |
|
|
| `Publish` | `MessageStructure=json` missing `default` key | `InvalidParameter` | 400 |
|
|
| `Publish` | `MessageStructure=json` message is not valid JSON | `InvalidParameter` | 400 |
|
|
| `GetPlatformApplicationAttributes` | Unknown ARN | `NotFound` | 404 |
|
|
| `GetEndpointAttributes` | Unknown ARN | `NotFound` | 404 |
|
|
| `SetEndpointAttributes` | Unknown ARN | `NotFound` | 404 |
|
|
|
|
`DeletePlatformApplication` and `DeleteEndpoint` are idempotent — they succeed
|
|
silently if the resource does not exist, matching real SNS behavior.
|