API Gateway
Floci supports both API Gateway v1 (REST APIs) and API Gateway v2 (HTTP APIs).
JWT issuer network policy
HTTP API JWT authorizers fetch the configured issuer's OIDC discovery document and JWKS. Floci rejects HTTP issuers and destinations that resolve to local, link-local, private, or other non-public addresses by default. This prevents an authorizer configuration from turning JWT verification into an SSRF path.
For an isolated development environment with a local fixture issuer, set
FLOCI_SECURITY_ALLOW_PRIVATE_JWT_TARGETS=true. This shared JWT policy also applies to AppSync
OIDC providers. The option permits private HTTPS targets and HTTP URLs that use a literal private
or loopback address. It does not permit public HTTP targets. Keep it disabled when Floci can
receive untrusted API configuration.
Custom API IDs
API IDs are generated randomly, which means endpoint URLs change every time you recreate an API. To pin
one, pass the reserved floci:override-id tag on creation and Floci uses its value as the API ID. This
works for both v1 (CreateRestApi) and v2 (CreateApi), and matches the tag other services such as KMS
and Cognito already use.
aws apigateway create-rest-api \
--name my-api \
--tags '{"floci:override-id":"my-fixed-id","env":"test"}' \
--endpoint-url http://localhost:4566
# the API is now reachable at the stable id "my-fixed-id"
The override key is consumed rather than stored, so it never appears in the tags the API returns. Any
other tags in the same request are kept. Because an ID cannot change after creation, an override that
differs from the API's ID is rejected with BadRequestException. Resending the current ID is accepted,
so CloudFormation and Terraform updates keep working.
Values must be non-blank and must not contain whitespace, control characters, or /, ?, #, since
those would break the endpoint URL. An invalid value is rejected with BadRequestException.
Creating a second API with an override ID that already exists in the region is rejected with
ConflictException instead of overwriting the existing API, matching how KMS and Cognito treat
duplicate override IDs.
[!NOTE] API Gateway previously used a
_custom_id_tag for this. It still works so existing setups keep running, and it is now stripped from the returned tags the same way, but it is deprecated: preferfloci:override-id. If both are present,floci:override-idwins, which lets you set both during a migration. The_custom_id_key is API Gateway specific and is not reserved for any other service.
API Gateway v1 (REST APIs)
Protocol: REST JSON
Endpoint: http://localhost:4566/restapis/...
ImportRestApi and PutRestApi return parser warnings in the REST API's warnings array.
Set the lowercase query parameter failonwarnings=true (SDK: failOnWarnings) to reject
a warning-bearing definition with BadRequestException before creating or changing
the API. The default is false. A successful import with no warnings clears any prior
warnings; malformed definitions and fatal import errors remain errors in either mode.
PutRestApi defaults to mode=merge. Merge keeps paths, methods, models, authorizers,
request validators, and gateway responses omitted from the incoming definition. An incoming
method replaces the complete existing method at the same path and HTTP verb, including its
integration and responses. Use mode=overwrite to clear those API definitions before import.
During merge, a method can reference a retained Lambda or Cognito authorizer even when the
incoming security schemes omit it. A same-name Lambda authorizer keeps its existing URI when
the incoming definition provides no new URI. If a same-name authorizer changes between Lambda
and Cognito, retained methods that reference it use the new authorization type. An explicit
operation-level security: [] leaves that method without authorization.
For a same-name Cognito authorizer, merge also keeps its existing pool ARNs when the incoming
definition provides no new providerARNs; an explicit list replaces them, including [].
REST OpenAPI imports resolve ${AWS::Region}, ${AWS::AccountId}, and ${AWS::Partition}
throughout the definition before parsing it. Values come from the request region and
account, so the same definition can be imported across regions and partitions. This
includes mapping templates and applies to both ImportRestApi and PutRestApi.
Supported Operations
| Category | Operations |
|---|---|
| APIs | CreateRestApi, ImportRestApi, PutRestApi, GetRestApi, GetRestApis, UpdateRestApi, DeleteRestApi |
| Resources | CreateResource, GetResource, GetResources, UpdateResource, DeleteResource |
| Methods | PutMethod, GetMethod, UpdateMethod, DeleteMethod |
| Method Responses | PutMethodResponse, GetMethodResponse, DeleteMethodResponse |
| Integrations | PutIntegration, GetIntegration, UpdateIntegration, DeleteIntegration |
| Integration Responses | PutIntegrationResponse, GetIntegrationResponse, UpdateIntegrationResponse, DeleteIntegrationResponse |
| Deployments | CreateDeployment, GetDeployment, GetDeployments, UpdateDeployment, DeleteDeployment |
| Stages | CreateStage, GetStage, GetStages, UpdateStage, DeleteStage, FlushStageAuthorizersCache |
| Authorizers | CreateAuthorizer, GetAuthorizer, GetAuthorizers, UpdateAuthorizer, DeleteAuthorizer |
| API Keys | CreateApiKey, ImportApiKeys, GetApiKey, GetApiKeys, UpdateApiKey, DeleteApiKey |
| Usage Plans | CreateUsagePlan, GetUsagePlan, GetUsagePlans, UpdateUsagePlan, DeleteUsagePlan, GetUsage |
| Usage Plan Keys | CreateUsagePlanKey, GetUsagePlanKey, GetUsagePlanKeys, DeleteUsagePlanKey |
| Request Validators | CreateRequestValidator, GetRequestValidator, GetRequestValidators, UpdateRequestValidator, DeleteRequestValidator |
| Gateway Responses | PutGatewayResponse, GetGatewayResponse, GetGatewayResponses, UpdateGatewayResponse, DeleteGatewayResponse |
| Models | CreateModel, GetModel, GetModels, UpdateModel, DeleteModel |
| Domain Names | CreateDomainName, GetDomainName, GetDomainNames, UpdateDomainName, DeleteDomainName |
| Base Path Mappings | CreateBasePathMapping, GetBasePathMapping, GetBasePathMappings, UpdateBasePathMapping, DeleteBasePathMapping |
| Account | GetAccount, UpdateAccount |
| Tags | TagResource, UntagResource, GetTags (ListTagsForResource) |
TagResource, UntagResource and GetTags also accept API key and usage plan ARNs,
arn:aws:apigateway:<region>::/apikeys/<id> and arn:aws:apigateway:<region>::/usageplans/<id>.
An ARN nested under /restapis/<id>/ other than a stage, such as a deployment or resource, is rejected with BadRequestException.
An ARN with an account segment is rejected with BadRequestException and an ARN from another region with NotFoundException, as on AWS.
Tagged REST APIs, stages, API keys, usage plans and custom domain names are discoverable through the
Resource Groups Tagging API, whose TagResources and UntagResources
write REST API, stage, API key, usage plan and custom domain name tags through to API Gateway.
API Key Behaviour Notes
CreateApiKey and ImportApiKeys share one route
Both operations are POST /apikeys, and AWS tells them apart by the query string, not by
Content-Type: ImportApiKeys carries ?mode=import&format=csv and a CSV body, CreateApiKey
carries no query parameters and a JSON body. The SDKs send ImportApiKeys with no Content-Type
header at all, so Floci accepts any media type on this route and dispatches on mode.
The CSV header row is addressed by name, not position. AWS's own column set is
Name,Key,Description,Enabled,UsagePlanIds; a Key column is required, and a missing Enabled
column defaults to true. Duplicate key values are reported in the warnings array, and
failonwarnings=true turns those warnings into a BadRequestException.
enabled defaults to false
CreateApiKey creates the key disabled when the request body has no enabled field, matching
AWS. AWS SDKs that model enabled as a plain boolean, such as the AWS SDK for Go v2, leave the field
out of the request when it is false, so a client that wants a usable key must send
enabled: true. An AWS::ApiGateway::ApiKey CloudFormation resource that omits Enabled is
likewise created, or updated, as disabled.
generateDistinctId
API key identifiers are generated independently from their secret values when generateDistinctId
is absent, matching current AWS behavior, or explicitly set to true. The deprecated explicit
generateDistinctId=false behavior is retained for compatibility and uses the key value as its
identifier. A caller-supplied value remains available only from create responses and reads that
explicitly request values.
Revocation
DeleteApiKey detaches the key from every usage plan before removing it, matching AWS. A usage plan key
stores its own copy of the key value, so without that sweep a deleted key would stay listed by
GetUsagePlanKeys and keep being recognised on the data plane.
requestContext.identity.apiKey is only populated when the x-api-key header matches a key that still
exists and has enabled set to true, so disabling a key through UpdateApiKey takes effect
immediately.
[!NOTE] Floci does not implement the
apiKeyRequiredgate on methods, so a request carrying an unknown, disabled, or deleted key is still executed — it simply arrives with a nullidentity.apiKeyrather than being rejected with403.
REST Lambda Authorizers
TOKEN and REQUEST authorizers must return a nonempty principalId and an IAM policy. Floci
checks all statements against execute-api:Invoke and the current method ARN, including
wildcards, conditions and explicit deny precedence. A policy that does not grant the method
returns 403. Invalid policies or non-scalar context values return 500. Scalar context keys,
including keys such as tenant-id, are retained in Lambda proxy events.
The alphanumeric and underscore restriction in the mapping reference applies to properties
referenced through $context.authorizer.property, not to the returned context map.
A missing TOKEN identity header returns 401 without invoking Lambda. For a REQUEST
authorizer with caching enabled, every configured identity source must be present and nonempty.
Creating or updating a cached REQUEST authorizer without an identity source returns 400.
The default TTL is 300 seconds, so omitting the TTL still requires an identity source.
With caching disabled, REQUEST authorizers receive the request even when identity sources
are missing. A Lambda Unauthorized error returns 401; other function errors return 500.
authorizerResultTtlInSeconds caches the validated policy, principal and context together.
A zero TTL disables caching. Cache entries are scoped to the account, region, API, stage,
deployment, authorizer configuration and ordered identity values. Each cache hit evaluates
the policy for the current method again. FlushStageAuthorizersCache clears a stage's entries
through DELETE /restapis/{apiId}/stages/{stageName}/cache/authorizers and returns 202.
A flush also invalidates pending invocations, so a result started before the flush cannot
repopulate that stage's cache afterward.
See the AWS Lambda authorizer workflow and response contract.
Custom Domain Names
A domain created with CreateDomainName is AVAILABLE at once. Its regionalDomainName is
<domain>.regional.local, and a request whose Host header is either that name or the domain
itself is routed through the domain's base path mappings to the mapped REST API stage. An
EDGE domain also reports a distributionDomainName under cloudfront.net and the fixed
CloudFront hosted zone Z2FDTNDATAQYW2; a REGIONAL domain reports neither, as on AWS. Moving a
domain between the two types with UpdateDomainName creates or drops the distribution.
GetDomainName returns the domainNameArn, the endpointConfiguration, both certificate ARNs
and the tags. Tags are managed through TagResource, UntagResource and GetTags on
arn:aws:apigateway:<region>::/domainnames/<domain>. A second mapping on a base path that
already has one is a ConflictException. A PRIVATE endpoint type is refused: private custom
domains are not emulated. Mutual TLS, ownership verification certificates, routing modes and
endpoint access modes are accepted and ignored.
With TLS enabled, CreateDomainName also adds the domain to Floci's server certificate, so
https://<domain> verifies against the ACM chain without a restart. The name must sit under a
local suffix such as localhost.floci.io; see TLS.
Templates can create domains and mappings with AWS::ApiGateway::DomainName and
AWS::ApiGateway::BasePathMapping; see CloudFormation.
IAM Authorization (AWS_IAM)
A method (v1) or route (v2) whose authorizationType is AWS_IAM requires a SigV4-signed caller.
Before the integration runs, the signature is verified against the request as it arrived (method,
path, query string, the headers named in SignedHeaders, and the SHA-256 of the body). A request
that reaches the API through a custom domain or an execute-api virtual
host is verified against the path the caller signed, not the /execute-api/... form Floci
rewrites it to internally. Both
placements AWS accepts are honoured: an Authorization header and a presigned query string
(X-Amz-Algorithm=AWS4-HMAC-SHA256). The credential must be scoped to the execute-api service.
Temporary credentials must also present the session token they were issued with, as
X-Amz-Security-Token (a header, or a query parameter on a presigned request). The token is
compared against the value recorded when STS minted the credential, so a missing or fabricated one
is rejected: the secret alone is not the whole credential. Whether the token is covered by
SignedHeaders is not required either way, since SigV4 leaves that service-specific.
Rejections are 403, and never reach the integration:
| Condition | REST (v1) message / x-amzn-ErrorType |
HTTP API (v2) |
|---|---|---|
| No SigV4 credentials at all | Missing Authentication Token / MissingAuthenticationTokenException |
Forbidden |
| Credentials present but incomplete, or scoped to another service | Incomplete Signature / IncompleteSignatureException |
Forbidden |
| Access key the emulator never issued | The security token included in the request is invalid. / UnrecognizedClientException |
Forbidden |
Temporary credential presenting no X-Amz-Security-Token, or one that is not the token it was issued |
The security token included in the request is invalid. / UnrecognizedClientException |
Forbidden |
X-Amz-Date outside the 5-minute window, or a presigned URL past X-Amz-Expires |
Signature expired / InvalidSignatureException |
Forbidden |
| Signature mismatch | The request signature we calculated does not match… / InvalidSignatureException |
Forbidden |
A verified caller reaches the integration as requestContext.identity.{accessKey, accountId, caller,
user, userArn} on a REST proxy event, and as requestContext.authorizer.iam on an HTTP API 2.0
event.
As on AWS, a REST (v1) AWS_PROXY or HTTP_PROXY integration never receives the caller's
Authorization header when the method is AWS_IAM, and on any method the header is dropped if it
carries a SigV4 signature. X-Amz-Date, X-Amz-Security-Token and every other header still pass
through, and a Bearer token on a method without AWS_IAM reaches the integration unchanged.
HTTP API (v2) integrations receive Authorization as the caller sent it.
Sign with any access key the emulator has issued (CreateAccessKey, or the temporary credentials
from AssumeRole), or with the well-known local-dev test/test pair that Floci accepts across
S3, RDS, and ElastiCache. No other unregistered key is accepted: an unknown key cannot sign for
itself. Any AWS SDK sends the session token automatically, so temporary credentials need no special
handling; a hand-rolled signer must include it.
[!NOTE] A session minted before Floci recorded session tokens (one restored from a persisted store written by an earlier version) has no issued token to compare against. Such a credential must still present a token, but its value cannot be checked. Re-minting it with
AssumeRolerestores the full check.[!NOTE] Floci authenticates the caller but does not authorize the call: it does not evaluate
execute-api:Invokeagainst the caller's IAM policies or a resource policy, so any validly signed, known caller is let through.principalOrgIdandcognitoIdentityare always null - Organizations membership and identity-pool federation are not modelled.
Gateway Responses
A REST API's gateway responses customise what the gateway itself answers when it, rather than
the integration, produces the response. All 21 AWS response types are accepted, keyed by
responseType; GetGatewayResponses lists every type, reporting the ones never customised with
defaultResponse: true, their AWS default statusCode, and the default
{"message":$context.error.messageString} template. PutGatewayResponse is an upsert,
UpdateGatewayResponse accepts add/replace/remove on /statusCode,
/responseParameters/<name> and /responseTemplates/<content-type> (JSON-pointer escaped, e.g.
application~1json), and DeleteGatewayResponse restores the default. The
x-amazon-apigateway-gateway-responses OpenAPI extension is imported by ImportRestApi and
PutRestApi, and AWS::ApiGateway::GatewayResponse is provisioned by CloudFormation.
On the execute plane every gateway-generated answer resolves the customisation for its type,
then for DEFAULT_4XX / DEFAULT_5XX, and applies the configured statusCode, the
gatewayresponse.header.* parameters ('static', method.request.header.*,
method.request.querystring.*, method.request.path.*, context.*, stageVariables.*) and the
responseTemplates (selected by the request's Accept header, falling back to
application/json), with $context.error.message, $context.error.messageString,
$context.error.responseType and $context.error.validationErrorString available to the template.
This is what lets a browser read a 401/403/400 as such instead of as a CORS failure once
DEFAULT_4XX maps Access-Control-Allow-Origin, exactly as the console's "Enable CORS" does.
| Gateway-generated answer | responseType |
|---|---|
No resource matches the path, or none declares the method (403 Missing Authentication Token) |
MISSING_AUTHENTICATION_TOKEN |
AWS_IAM method without a signature |
MISSING_AUTHENTICATION_TOKEN |
AWS_IAM signature malformed or mismatching |
INVALID_SIGNATURE |
AWS_IAM signature outside the 5-minute window, or a presigned URL past its expiry |
EXPIRED_TOKEN |
AWS_IAM access key the emulator never issued |
ACCESS_DENIED |
Lambda authorizer returns Deny |
ACCESS_DENIED |
| Lambda authorizer fails or throws | AUTHORIZER_FAILURE |
Method with apiKeyRequired and no usable x-api-key (403 Forbidden) |
INVALID_API_KEY |
| Request validator rejects a parameter / the body | BAD_REQUEST_PARAMETERS / BAD_REQUEST_BODY |
passthroughBehavior rejects the request Content-Type (415) |
UNSUPPORTED_MEDIA_TYPE |
| Missing or unresolvable integration or URI, MOCK template that does not render | API_CONFIGURATION_ERROR |
| Lambda proxy function error, malformed proxy payload, or missing function | INTEGRATION_FAILURE |
A customised statusCode overrides the status Floci would otherwise send. Throttling, quota, request size and WAF answers are not produced on the execute plane, so
THROTTLED, QUOTA_EXCEEDED, REQUEST_TOO_LARGE and WAF_FILTERED can be configured but never
fire, and an HTTP/HTTP_PROXY backend's own status is relayed rather than treated as a gateway
response.
Not Implemented
These management-plane operations have no handler in v1. Calls will return 404 or an error:
- Authorizer testing:
TestInvokeAuthorizer - Model templates:
GetModelTemplate - Documentation parts and versions (the entire family, 10 operations)
- Client Certificates (5 operations)
GetExport/ImportDocumentationParts
The execute plane (actual proxied HTTP traffic via /restapis/{id}/{stage}/_user_request_/…)
is implemented separately and is not counted as management-plane operations. A deployed REST
API is also available at http://{apiId}.execute-api.localhost.floci.io:4566/{stage}/{path}
(or with the region label, {apiId}.execute-api.{region}.localhost.floci.io)
and /execute-api/{apiId}/{stage}/{path}. All three forms use the same method authorization
and mappings. It supports these integration types; others return an error:
| Type | Support |
|---|---|
AWS_PROXY (Lambda proxy) |
✅ |
AWS (Lambda / AWS service with VTL request/response templates) |
✅ |
HTTP_PROXY (passthrough to an arbitrary HTTP backend) |
✅ |
HTTP (non-proxy, with VTL request/response templates) |
✅ |
MOCK |
✅ |
A MOCK integration renders its request template and uses the statusCode it produces to pick the integration response, exactly as AWS does: the first response whose selectionPattern matches wins, otherwise the response without a pattern (the default) answers. This is what makes CORS preflights declared with a 204 response (CDK's addCorsPreflight) carry their Access-Control-* headers.
HTTP_PROXY forwards the request to the integration's uri — with {param} placeholders resolved from the matched resource's path parameters — and relays the backend's status, headers and body unchanged. Per AWS, no request templates and no integration-response selection apply to HTTP_PROXY, so a backend 4xx/5xx reaches the caller verbatim rather than being remapped. integration.request.{header,querystring,path}.* → method.request.* mappings are applied. Hop-by-hop headers (including Host) are stripped. An unreachable or failing backend yields 502.
A greedy resource captures only the path segments after its parent. For example,
/api/v1/auth/signup/{proxy+} captures checkpoint/status from
/api/v1/auth/signup/checkpoint/status, so an integration URI ending in
/api/v1/auth/signup/{proxy} forwards the prefix once. Greedy parameter names other
than proxy are supported, including in integration.request.path.* mappings.
REST HTTP_PROXY and HTTP integrations also accept context.* sources in header, query
and path parameter mappings. Supported request fields are accountId, apiId, deploymentId,
httpMethod, path (including the stage), protocol, requestId, resourceId, resourcePath,
stage, identity.sourceIp and identity.userAgent. context.requestId uses the same ID as
the HTTP integration's VTL templates. context.authorizer.principalId and
context.authorizer.<property> come from the successful authorizer result; authenticated Cognito
claims are available as context.authorizer.claims.<property>. String, number and boolean values
are forwarded as strings. Missing values and objects are not mapped. An explicit header mapping
replaces all inbound values of that header, regardless of casing.
For a header mapped from context.*, inbound values are also removed when the context value is
missing or is not a scalar. The backend therefore cannot receive a client-supplied value in place
of an absent authorizer claim. An unresolved method.request.* mapping retains the existing
HTTP proxy passthrough behavior.
method.request.* sources always read the original inbound headers, query parameters and path
parameters. Mapped destinations do not change another mapping's source, regardless of mapping order.
Use alphanumeric or underscore authorizer context keys, as required by the
AWS mapping contract.
For example, return userClaims in the authorizer context and map
integration.request.header.X-User-Claims from context.authorizer.userClaims. The HTTP header
name can contain hyphens. REST Lambda authorizer policy evaluation, response validation and
result caching are handled separately from these integration mappings.
A backend response body larger than the 10 MB API Gateway payload quota yields 413 with {"message":"Request Entity Too Large"}. The same limit applies to HTTP API HTTP_PROXY integrations.
Passthrough keeps repeated values repeated, in both directions: ?tag=a&tag=b reaches the backend as two tag parameters rather than one tag=a,b, a header sent twice arrives twice, and a backend that returns two Set-Cookie headers relays two to the caller. Comma-joining them would not be reversible, since a cookie's Expires attribute contains a comma of its own. An explicit integration.request.header.X or integration.request.querystring.X mapping overwrites, so it replaces any repeated inbound values with the single mapped one.
HTTP (non-proxy) transforms in both directions instead:
- Request — the body is the rendered
requestTemplatesentry selected by the incomingContent-Type(falling back to the type without its charset), subject topassthroughBehavior(NEVERandWHEN_NO_TEMPLATESreturn415). Only headers and query parameters named byintegration.request.*mappings are forwarded; unmapped inbound headers are not passed through — that passthrough isHTTP_PROXY's job. - Response — the backend's reply runs through the method's integration responses. As in AWS,
selectionPatternis matched against the backend's HTTP status code (forAWS/Lambda integrations it is matched against the error message instead), so"5\\d{2}"on a502integration response remaps any backend5xxto502. The matched response'sresponseTemplatesrender the body,responseParametersmapintegration.response.header.*(case-insensitively) orintegration.response.body.<jsonpath>ontomethod.response.header.*, and$context.responseOverrideassignments take precedence. With no integration responses configured, the backend's status and body are relayed as-is.
Integration Settings
PutIntegration persists and GetIntegration returns the full configuration, including the mapping templates and integration responses that IaC tools diff against:
| Field | Behaviour |
|---|---|
requestParameters / requestTemplates |
Applied at invoke time and returned on read-back |
passthroughBehavior |
NEVER and WHEN_NO_TEMPLATES reject an unmatched Content-Type with 415 |
timeoutInMillis |
Honoured; defaults to AWS's 29,000 ms. Values below 50 are rejected. The 29s ceiling is an edge-optimized limit, so Regional APIs may exceed it |
responseTransferMode |
Defaults to BUFFERED. STREAM is accepted only for HTTP_PROXY and AWS_PROXY integrations and is stored and returned by the management API, but execution responses are still buffered; response streaming is not yet implemented |
tlsConfig.insecureSkipVerification |
Honoured, with AWS's semantics: it stops requiring the backend certificate to be issued by a trusted CA, so a private-CA or self-signed backend is reachable, but expiration, hostname and the presence of a root certificate authority are still checked |
contentHandling |
CONVERT_TO_TEXT base64-encodes a binary request for mapping templates; CONVERT_TO_BINARY base64-decodes a text request before sending it |
connectionType / connectionId |
VPC_LINK requires connectionId to name an existing, available VPC link; an unknown link yields 502 |
cacheNamespace / cacheKeyParameters |
Form the response cache key (see below) |
credentials |
Persisted and returned. Floci does not enforce IAM, so the role is not actually assumed |
Integration responses additionally accept contentHandling, applied as an output conversion after response templates.
Binary Payloads
Set binaryMediaTypes on the RestApi to mark content types as binary. Entries are matched exactly, ignoring any charset parameter; */* is the one wildcard entry, and it covers every content type. A subtype wildcard such as image/* is not expanded, matching AWS, which documents only */* and otherwise names one exact media type at a time. A binary request body reaches an AWS_PROXY (Lambda) integration base64-encoded with isBase64Encoded: true; previously it was read as a UTF-8 string, which corrupted it. For non-proxy integrations, pair binaryMediaTypes with contentHandling as above.
Caching
Response caching needs both switches AWS requires: cacheClusterEnabled on the stage and caching/enabled on the method (or the */* wildcard) via UpdateStage patch operations. Entries are keyed by the integration's cacheNamespace and the values of its cacheKeyParameters, and by nothing else: AWS lets separate resources share a cacheNamespace precisely so they can return the same cached data, so the method and request path are deliberately not part of the key. The namespace defaults to the resource id, which keeps resources that did not opt into sharing separate. Entries expire after caching/ttlInSeconds (default 300s), and only successful (< 400) responses are stored. There is no real cache cluster — cacheClusterSize is recorded and reported but has no effect on capacity.
VPC Links
The five REST VPC Link operations (CreateVpcLink, GetVpcLink, GetVpcLinks, UpdateVpcLink, DeleteVpcLink) are emulated at /vpclinks. CreateVpcLink requires a name and at least one target ARN, answers 202, and provisions the link as AVAILABLE immediately rather than transitioning through PENDING. Since Floci has no real VPC, a valid link routes straight to the integration URI; what is enforced is that the link exists and is available. UpdateVpcLink follows AWS's patch-operation table: only replace on /name and /description is accepted, and any other operation or path returns BadRequestException rather than being applied or silently ignored.
Examples
export AWS_ENDPOINT_URL=http://localhost:4566
# Create a REST API
API_ID=$(aws apigateway create-rest-api \
--name "My API" \
--query id --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Get the root resource
ROOT_ID=$(aws apigateway get-resources \
--rest-api-id $API_ID \
--query 'items[?path==`/`].id' --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Create a resource
RESOURCE_ID=$(aws apigateway create-resource \
--rest-api-id $API_ID \
--parent-id $ROOT_ID \
--path-part users \
--query id --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Add a GET method
aws apigateway put-method \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method GET \
--authorization-type NONE \
--endpoint-url $AWS_ENDPOINT_URL
# Add a Lambda integration
aws apigateway put-integration \
--rest-api-id $API_ID \
--resource-id $RESOURCE_ID \
--http-method GET \
--type AWS_PROXY \
--integration-http-method POST \
--uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:my-function/invocations" \
--endpoint-url $AWS_ENDPOINT_URL
# Deploy to a stage
aws apigateway create-deployment \
--rest-api-id $API_ID \
--stage-name dev \
--endpoint-url $AWS_ENDPOINT_URL
# Call the deployed API
curl http://localhost:4566/restapis/$API_ID/dev/_user_request_/users
Usage reporting
GetUsage returns the real response envelope: a values map of API key id to one [used, remaining]
pair per day of the inclusive range, alongside usagePlanId, startDate and endDate. The second
element of each pair is the quota limit minus cumulative use on real API Gateway, not the quota
itself.
The operation pages over the API key entries with limit and position, defaulting to 25 keys a
page and emitting position only when another page exists.
Request acceptance and response page size are separate things here. Probed against real API
Gateway, every limit from 500 up to Integer.MAX_VALUE is accepted without error, so none is
rejected. That does not show the service ever returning more than 500 entries in one page, and the
documented contract caps a page at 500, so a larger limit is honoured as a request while the page
returned stays capped at 500.
The lower bound is a deliberate divergence: real API Gateway answers limit=0 and limit=-1 with
an InternalFailure, which is a fault rather than a contract, so a page size below one is rejected
as a BadRequestException instead of reproducing a 500.
Both numbers are always zero. Nothing meters requests per API key, and a usage plan stores no quota to subtract from, so there is no limit to report against. Throttle settings are likewise accepted and stored but never enforced. Storing a quota on the usage plan and counting on the execute path are the two pieces still missing; a caller that sums the used counts gets zero, which is what it already got before the action existed, without having to special-case a missing endpoint.
Usage Plan Tags and Custom IDs
Usage plans accept arbitrary tags, and the same reserved floci:override-id tag used for
custom API IDs pins the plan's id:
# Create a usage plan with a custom ID and additional tags
aws apigateway create-usage-plan \
--name "my-plan" \
--tags '{"floci:override-id":"my-plan-id","env":"staging"}' \
--endpoint-url $AWS_ENDPOINT_URL
# The plan is now accessible at its custom ID
aws apigateway get-usage-plans --endpoint-url $AWS_ENDPOINT_URL
The override key is validated and consumed exactly as it is for CreateRestApi, so it never appears in
the tags a usage plan returns. The deprecated _custom_id_ key is still honored on create for existing
setups, and floci:override-id wins when both are present. Every other tag is persisted and returned in
CreateUsagePlan and GetUsagePlans responses.
Configuration
| Variable | Default | Description |
|---|---|---|
FLOCI_SERVICES_APIGATEWAY_ENABLED |
true |
Enable or disable API Gateway v1 (REST APIs) |
FLOCI_SERVICES_APIGATEWAYV2_ENABLED |
true |
Enable or disable API Gateway v2 (HTTP and WebSocket APIs) |
FLOCI_SERVICES_APIGATEWAY_VTL_MAX_LOOPS |
10000 |
Maximum #foreach iterations a VTL mapping template may execute |
FLOCI_SERVICES_APIGATEWAY_VTL_MAX_OUTPUT_CHARS |
1048576 |
Maximum characters a VTL mapping template may render |
FLOCI_SERVICES_APIGATEWAY_VTL_TIMEOUT_MILLIS |
5000 |
Maximum wall-clock time a VTL mapping template may spend evaluating |
VTL (Velocity Template Language) mapping templates render inside a reflection-restricted sandbox
(SecureUberspector, with Class, ClassLoader, Runtime, ProcessBuilder, System, Thread,
java.io.File and related classes/packages blocked) and are subject to the three limits above.
The loop cap truncates a #foreach at the configured iteration count and lets the template finish
rendering with whatever output it produced up to that point; it does not fail the template.
Exceeding the output-size or execution-time limit does fail the template, the same way any other
Velocity evaluation error does; neither introduces a new error shape. This applies to both API
Gateway v1 (AWS/Lambda mapping templates) and the AppSync resolver templates described in
appsync.md.
API Gateway v2 (HTTP and WebSocket APIs)
Protocol: REST JSON
Endpoint: http://localhost:4566/v2/apis/...
Both HTTP and WebSocket protocol types are fully supported, including the WebSocket data-plane (real connection handling, message routing, and the @connections management API).
HTTP API data-plane
API Gateway v2 advertises HTTP APIs through Floci's local execute-api domain:
When an API has a $default stage, callers may omit the stage segment:
The region-bearing form a client derives from a stage's invoke URL is accepted on the
same domain, as it is on localhost and the configured FLOCI_HOSTNAME:
APIs created or updated with disableExecuteApiEndpoint reject requests to
this default hostname with 404 Not Found, matching AWS HTTP API behavior.
Routes carrying authorizationType: AWS_IAM: including those an OpenAPI import resolves from an
awsSigv4 security scheme: require a signed caller; see
IAM Authorization.
Supported Operations
| Category | Operations |
|---|---|
| APIs | CreateApi, GetApi, GetApis, UpdateApi, DeleteApi, DeleteCorsConfiguration, ImportApi, ReimportApi |
| Routes | CreateRoute, GetRoute, GetRoutes, UpdateRoute, DeleteRoute |
| Route Responses | CreateRouteResponse, GetRouteResponse, GetRouteResponses, UpdateRouteResponse, DeleteRouteResponse |
| Integrations | CreateIntegration, GetIntegration, GetIntegrations, UpdateIntegration, DeleteIntegration |
| Integration Responses | CreateIntegrationResponse, GetIntegrationResponse, GetIntegrationResponses, UpdateIntegrationResponse, DeleteIntegrationResponse |
| Authorizers | CreateAuthorizer, GetAuthorizer, GetAuthorizers, UpdateAuthorizer, DeleteAuthorizer |
| Stages | CreateStage, GetStage, GetStages, UpdateStage, DeleteStage |
| Deployments | CreateDeployment, GetDeployment, GetDeployments, UpdateDeployment, DeleteDeployment |
| Models | CreateModel, GetModel, GetModels, UpdateModel, DeleteModel |
| Domain Names | CreateDomainName, GetDomainName, GetDomainNames, DeleteDomainName |
| API Mappings | CreateApiMapping, GetApiMapping, GetApiMappings, DeleteApiMapping |
| VPC Links | CreateVpcLink, GetVpcLink, GetVpcLinks, DeleteVpcLink |
| Tags | TagResource, UntagResource, GetTags |
WebSocket Data-Plane
Floci supports real WebSocket connections for API Gateway v2 WebSocket APIs. Clients connect via:
Supported Features
| Feature | Status |
|---|---|
$connect route with Lambda integration |
✅ |
$disconnect route with Lambda integration |
✅ |
$default route (fallback) |
✅ |
Custom routes via routeSelectionExpression |
✅ |
| Route response selection expression | ✅ |
Lambda REQUEST authorizer on $connect |
✅ |
| Identity source validation (header/querystring) | ✅ |
@connections POST (send message to client) |
✅ |
@connections GET (get connection info) |
✅ |
@connections DELETE (disconnect client) |
✅ |
| Stage variable substitution in integration URIs | ✅ |
| AWS_PROXY integration (Lambda) | ✅ |
| AWS integration (Lambda with VTL templates) | ✅ |
| HTTP_PROXY integration | ✅ |
| HTTP integration (with VTL templates) | ✅ |
| MOCK integration | ✅ |
| GoneException (410) for disconnected connections | ✅ |
Binary frame support (isBase64Encoded: true) |
✅ |
$connect response headers propagation |
✅ |
| 128 KB payload size limit enforcement | ✅ |
| 10-minute idle timeout | ✅ |
| 2-hour max connection duration | ✅ |
@connections Management API
The @connections API allows server-side code (e.g., Lambda functions) to send messages to connected clients, retrieve connection metadata, or disconnect clients:
POST /execute-api/{apiId}/{stageName}/@connections/{connectionId} — Send message
GET /execute-api/{apiId}/{stageName}/@connections/{connectionId} — Get connection info
DELETE /execute-api/{apiId}/{stageName}/@connections/{connectionId} — Disconnect client
Behavior Notes
- Connection URL: Floci accepts the AWS-style execute-api host as well as the path form. All of these reach the same WebSocket API:
ws://{apiId}.execute-api.{region}.localhost:4566/{stage}— region-bearing, mirroring AWS'swss://{api-id}.execute-api.{region}.amazonaws.com/{stage}ws://{apiId}.execute-api.localhost.floci.io:4566/{stage}— Floci's built-in execute-api domain (regionless; the region is resolved by an apiId lookup)ws://localhost:4566/ws/{apiId}/{stage}— the explicit path form
The @connections management API is likewise reachable on the execute-api host (http://{apiId}.execute-api.{region}.localhost:4566/{stage}/@connections/{connectionId}).
- Idle timeout: 10 minutes (matching AWS default). Not configurable per-API.
- Max connection duration: 2 hours (matching AWS). Connections are closed automatically.
- Payload size limit: 128 KB per frame (matching AWS). Oversized messages receive an error frame.
Not Implemented
ExportApi,UpdateDomainName,UpdateApiMappingUpdateVpcLink— the other four VPC Link operations are implemented; see the table above
Examples
export AWS_ENDPOINT_URL=http://localhost:4566
# Create an HTTP API
API_ID=$(aws apigatewayv2 create-api \
--name "My HTTP API" \
--protocol-type HTTP \
--query ApiId --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Create a Lambda integration
INTEGRATION_ID=$(aws apigatewayv2 create-integration \
--api-id $API_ID \
--integration-type AWS_PROXY \
--integration-uri "arn:aws:lambda:us-east-1:000000000000:function:my-function" \
--payload-format-version 2.0 \
--query IntegrationId --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Create a route
aws apigatewayv2 create-route \
--api-id $API_ID \
--route-key "GET /users" \
--target "integrations/$INTEGRATION_ID" \
--endpoint-url $AWS_ENDPOINT_URL
# Deploy
aws apigatewayv2 create-stage \
--api-id $API_ID \
--stage-name dev \
--auto-deploy \
--endpoint-url $AWS_ENDPOINT_URL
WebSocket API
export AWS_ENDPOINT_URL=http://localhost:4566
# Create a WebSocket API
WS_API_ID=$(aws apigatewayv2 create-api \
--name "My WebSocket API" \
--protocol-type WEBSOCKET \
--route-selection-expression '$request.body.action' \
--query ApiId --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Create a Lambda integration
WS_INTEGRATION_ID=$(aws apigatewayv2 create-integration \
--api-id $WS_API_ID \
--integration-type AWS_PROXY \
--integration-uri "arn:aws:lambda:us-east-1:000000000000:function:my-ws-handler" \
--query IntegrationId --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Create $connect, $disconnect, and $default routes
aws apigatewayv2 create-route \
--api-id $WS_API_ID \
--route-key '$connect' \
--target "integrations/$WS_INTEGRATION_ID" \
--endpoint-url $AWS_ENDPOINT_URL
aws apigatewayv2 create-route \
--api-id $WS_API_ID \
--route-key '$disconnect' \
--target "integrations/$WS_INTEGRATION_ID" \
--endpoint-url $AWS_ENDPOINT_URL
aws apigatewayv2 create-route \
--api-id $WS_API_ID \
--route-key '$default' \
--route-response-selection-expression '$default' \
--target "integrations/$WS_INTEGRATION_ID" \
--endpoint-url $AWS_ENDPOINT_URL
# Deploy
aws apigatewayv2 create-stage \
--api-id $WS_API_ID \
--stage-name prod \
--endpoint-url $AWS_ENDPOINT_URL
# Connect via WebSocket (using wscat or any WebSocket client)
# wscat -c ws://localhost:4566/ws/$WS_API_ID/prod
# Send a message to a connected client via @connections API
# curl -X POST http://localhost:4566/execute-api/$WS_API_ID/prod/@connections/$CONNECTION_ID \
# -d "Hello from server"
# Get connection info
# curl http://localhost:4566/execute-api/$WS_API_ID/prod/@connections/$CONNECTION_ID
# Disconnect a client
# curl -X DELETE http://localhost:4566/execute-api/$WS_API_ID/prod/@connections/$CONNECTION_ID