{"record":{"id":"d34c8f9708854a4f","repo":"floci-io/floci","slug":"badrequestexception-d34c8f","errorCode":"BadRequestException","errorMessage":"RouteSelectionExpression is required for WEBSOCKET protocol","messagePattern":"RouteSelectionExpression is required for WEBSOCKET protocol","errorType":"validation","errorClass":"AwsException","httpStatus":400,"severity":"error","filePath":"src/main/java/io/github/hectorvent/floci/services/apigatewayv2/ApiGatewayV2Service.java","lineNumber":73,"sourceCode":"                new TypeReference<>() {});\n        this.modelStore = storageFactory.create(\"apigatewayv2\", \"apigatewayv2-models.json\",\n                new TypeReference<>() {});\n        this.vpcLinkStore = storageFactory.create(\"apigatewayv2\", \"apigatewayv2-vpclinks.json\",\n                new TypeReference<>() {});\n        this.regionResolver = regionResolver;\n    }\n\n    // ──────────────────────────── API CRUD ────────────────────────────\n\n    public Api createApi(String region, Map<String, Object> request) {\n        String name = (String) request.get(\"name\");\n        String protocolType = (String) request.getOrDefault(\"protocolType\", \"HTTP\");\n        String routeSelectionExpression = (String) request.get(\"routeSelectionExpression\");\n        String description = (String) request.get(\"description\");\n        String apiKeySelectionExpression = (String) request.get(\"apiKeySelectionExpression\");\n\n        if (\"WEBSOCKET\".equals(protocolType) && (routeSelectionExpression == null || routeSelectionExpression.isBlank())) {\n            throw new AwsException(\"BadRequestException\",\n                    \"RouteSelectionExpression is required for WEBSOCKET protocol\", 400);\n        }\n\n        // Apply AWS defaults\n        if (apiKeySelectionExpression == null) {\n            apiKeySelectionExpression = \"$request.header.x-api-key\";\n        }\n        if (\"HTTP\".equals(protocolType) && routeSelectionExpression == null) {\n            routeSelectionExpression = \"${request.method} ${request.path}\";\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        Map<String, String> tags = (Map<String, String>) request.get(\"tags\");\n        String overrideId = ReservedTags.extractOverrideApiId(tags);\n        String apiId = overrideId != null ? overrideId : shortId(10);\n        if (apiStore.get(apiKey(region, apiId)).isPresent()) {\n            throw new AwsException(\"ConflictException\",\n                    \"API with id '\" + apiId + \"' already exists\", 409);","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/floci-io/floci/blob/62ff490619e7bd3554597c28c704081b4c15add5/src/main/java/io/github/hectorvent/floci/services/apigatewayv2/ApiGatewayV2Service.java#L55-L91","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Add --route-selection-expression '$request.body.action' (or whatever field routes your frames) to the create-api call.","In IaC, ensure route_selection_expression is set on every AWS::ApiGatewayV2::Api whose ProtocolType is WEBSOCKET.","Verify the JSON key casing is exactly 'routeSelectionExpression'.","Keep HTTP APIs unchanged — they receive the default '${request.method} ${request.path}' automatically."],"exampleFix":"# before\naws apigatewayv2 create-api --name chat --protocol-type WEBSOCKET\n\n# after\naws apigatewayv2 create-api --name chat --protocol-type WEBSOCKET \\\n  --route-selection-expression '$request.body.action'","handlingStrategy":"validation","validationCode":"// Validate before createApi\nString protocolType = request.getOrDefault(\"protocolType\", \"HTTP\").toString();\nString rse = (String) request.get(\"routeSelectionExpression\");\nif (\"WEBSOCKET\".equals(protocolType) && (rse == null || rse.isBlank())) {\n    request.put(\"routeSelectionExpression\", \"$request.body.action\"); // or fail fast\n}\napiGatewayV2.createApi(region, request);","typeGuard":"boolean canCreateApi(Map<String, Object> req) {\n    String pt = (String) req.getOrDefault(\"protocolType\", \"HTTP\");\n    String rse = (String) req.get(\"routeSelectionExpression\");\n    return !\"WEBSOCKET\".equals(pt) || (rse != null && !rse.isBlank());\n}","tryCatchPattern":null,"preventionTips":["In IaC, make route_selection_expression a required attribute when ProtocolType is WEBSOCKET.","Add a schema check in the deploy pipeline that rejects WEBSOCKET APIs without the expression.","Standardize on '$request.body.action' unless routing on a different payload field."],"tags":["apigatewayv2","websocket","create-api","validation"],"backgroundTag":null,"analyzedSha":"62ff490619e7bd3554597c28c704081b4c15add5","analyzedAt":"2026-08-14T14:25:23.764Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}