floci-io/floci · error · AwsException

BadRequestException

BadRequestException

Error message

RouteSelectionExpression is required for WEBSOCKET protocol

What it means

Thrown by ApiGatewayV2Service.createApi() when protocolType is WEBSOCKET and routeSelectionExpression is absent or blank. AWS requires WebSocket APIs to declare how a frame's JSON payload selects a route (commonly '$request.body.action'), because unlike HTTP APIs there is no method/path to derive the route from. HTTP APIs get a default expression ('${request.method} ${request.path}') and never hit this check; WebSocket APIs have no safe default, so the request is rejected with HTTP 400 before any id is allocated.

Source

Thrown at src/main/java/io/github/hectorvent/floci/services/apigatewayv2/ApiGatewayV2Service.java:73

                new TypeReference<>() {});
        this.modelStore = storageFactory.create("apigatewayv2", "apigatewayv2-models.json",
                new TypeReference<>() {});
        this.vpcLinkStore = storageFactory.create("apigatewayv2", "apigatewayv2-vpclinks.json",
                new TypeReference<>() {});
        this.regionResolver = regionResolver;
    }

    // ──────────────────────────── API CRUD ────────────────────────────

    public Api createApi(String region, Map<String, Object> request) {
        String name = (String) request.get("name");
        String protocolType = (String) request.getOrDefault("protocolType", "HTTP");
        String routeSelectionExpression = (String) request.get("routeSelectionExpression");
        String description = (String) request.get("description");
        String apiKeySelectionExpression = (String) request.get("apiKeySelectionExpression");

        if ("WEBSOCKET".equals(protocolType) && (routeSelectionExpression == null || routeSelectionExpression.isBlank())) {
            throw new AwsException("BadRequestException",
                    "RouteSelectionExpression is required for WEBSOCKET protocol", 400);
        }

        // Apply AWS defaults
        if (apiKeySelectionExpression == null) {
            apiKeySelectionExpression = "$request.header.x-api-key";
        }
        if ("HTTP".equals(protocolType) && routeSelectionExpression == null) {
            routeSelectionExpression = "${request.method} ${request.path}";
        }

        @SuppressWarnings("unchecked")
        Map<String, String> tags = (Map<String, String>) request.get("tags");
        String overrideId = ReservedTags.extractOverrideApiId(tags);
        String apiId = overrideId != null ? overrideId : shortId(10);
        if (apiStore.get(apiKey(region, apiId)).isPresent()) {
            throw new AwsException("ConflictException",
                    "API with id '" + apiId + "' already exists", 409);

View on GitHub (pinned to 62ff490619)

Solutions

  1. Add --route-selection-expression '$request.body.action' (or whatever field routes your frames) to the create-api call.
  2. In IaC, ensure route_selection_expression is set on every AWS::ApiGatewayV2::Api whose ProtocolType is WEBSOCKET.
  3. Verify the JSON key casing is exactly 'routeSelectionExpression'.
  4. Keep HTTP APIs unchanged — they receive the default '${request.method} ${request.path}' automatically.

Example fix

# before
aws apigatewayv2 create-api --name chat --protocol-type WEBSOCKET

# after
aws apigatewayv2 create-api --name chat --protocol-type WEBSOCKET \
  --route-selection-expression '$request.body.action'
Defensive patterns

Strategy: validation

Validate before calling

// Validate before createApi
String protocolType = request.getOrDefault("protocolType", "HTTP").toString();
String rse = (String) request.get("routeSelectionExpression");
if ("WEBSOCKET".equals(protocolType) && (rse == null || rse.isBlank())) {
    request.put("routeSelectionExpression", "$request.body.action"); // or fail fast
}
apiGatewayV2.createApi(region, request);

Type guard

boolean canCreateApi(Map<String, Object> req) {
    String pt = (String) req.getOrDefault("protocolType", "HTTP");
    String rse = (String) req.get("routeSelectionExpression");
    return !"WEBSOCKET".equals(pt) || (rse != null && !rse.isBlank());
}

Prevention

When it happens

Trigger: aws apigatewayv2 create-api --name ws-api --protocol-type WEBSOCKET with no --route-selection-expression flag. Or passing the expression under a misspelled key in the JSON body (routeSelectionExpression vs routeselectionexpression) so the map lookup returns null. Passing whitespace-only strings also triggers it because of the isBlank() check.

Common situations: Copy-pasting an HTTP API creation command and only changing protocolType. Terraform/CDK templates that conditionally set route_selection_expression and skip it for WebSocket resources. Assuming an emulator will default the field the way it defaults apiKeySelectionExpression.

Understand the failure class

Background: BadRequestException (HTTP 400) — NestJS 'Bad Request' Errors: Why They Fire and How to Fix Them — this error's family across 4 libraries.

Related errors


AI-assisted analysis of floci-io/floci@62ff490619 (2026-08-14). Data as JSON: /api/errors/d34c8f9708854a4f. Report an issue: GitHub.