Reputation: 19329
When the client sends a websocket message to AWS websocket API Gateway the websocket Lambda handler function receives three arguments: event
, context
and callback
:
import type { APIGatewayProxyEvent, Context, } from "aws-lambda";
export class Handler {
constructor() {}
async execute(event: APIGatewayProxyEvent, context: Context, callback: any): Promise<void> {
console.log(`${event}, ${context}, ${callback}`);
if (!event.requestContext || !event.requestContext.connectionId) {
callback(null, { statusCode: 400, body: "Missing RequestContext" });
};
callback(null, {statusCode: 200, body: 'Your message was received'});
}
}
const Handler = new Handler(service);
async function handler(event: APIGatewayProxyEvent, context: Context, callback: any): Promise<void> {
return await Handler.execute(event, context, callback);
}
export default handler;
The Handler checks if the event
that it received contains the value for event.requestContext.connectionId
. If not, it calls the callback function passing null
as the first argument and a message containing the 400 statusCode
and body
with message telling that the connectionId is missing:
callback(null, { statusCode: 400, body: "Missing RequestContext" });
Unfortunately, I was not able to locate any AWS documentation describing how exactly we should be using this callback
function. Instead I've used some code examples found on Internet.
I was expecting that calling the callback
function passing it 400 status
will make the Handler function exit and raise an exception. But it doesn't happen. The handler just keeps going executing the next lines like nothing happened and callback function was never called.
callback
function?Please post your answer as a script example or as a snippet so we could test it.
P.S. The doc posted at https://aws.amazon.com/blogs/compute/announcing-websocket-apis-in-amazon-api-gateway/ says that:
The backend can send messages to specific users via a callback URL that is provided after the user is connected to the WebSocket API.
And there is a snippet showing it:
exports.handler = function(event, context, callback) {
var putParams = {
TableName: process.env.TABLE_NAME,
Item: {
connectionId: { S: event.requestContext.connectionId }
}
};
DDB.putItem(putParams, function(err, data) {
callback(null, {
statusCode: err ? 500 : 200,
body: err ? "Failed to connect: " + JSON.stringify(err) : "Connected"
});
});
};
Upvotes: 1
Views: 2777
Reputation: 25679
There are a few important things to understand about two-way communication patterns using Lambda integrations for Websocket APIs:
"Callback" is an overloaded term. First, it refers to the legacy pattern for Nodejs Lambda function handlers. We should factor this out in favour of the AWS-recommended async-await handler pattern. Second, AWS has sometimes referred to "Callback URLs" as way for backends to communicate with Websocket clients using the @aws-sdk/client-apigatewaymanagementapi client1. The docs also refer to the same thing as @connections commands.
One problem with your code is that you are mixing up the unrelated Lambda and Websocket API "callback" concepts. To avoid ambiguity, I will avoid the term "callback" altogether.
There are two ways to send data from backend services to connected clients:
$default
route integration response with a RouteResponse
For the $default
route only, your Lambda can return a message to a client in the handler response. This will only work if you have added a RouteResponse to the route2.
// default-route-handler.ts
import { APIGatewayProxyWebsocketEventV2, APIGatewayProxyResult } from 'aws-lambda';
export async function handler(event: APIGatewayProxyWebsocketEventV2): Promise<APIGatewayProxyResult> {
return {
statusCode: 200,
body: JSON.stringify('hello client, I am the $default route!'),
};
}
For all routes, you can use the @connections API to send a POST request. Do this with the ApiGatewayManagementApi client's PostToConnection
API, which handles the heavy lifting of sending the message to the client. The Lambda just returns a status.
// another-great-handler.ts
import { APIGatewayProxyWebsocketEventV2 } from 'aws-lambda';
import { ApiGatewayManagementApiClient, PostToConnectionCommand } from '@aws-sdk/client-apigatewaymanagementapi';
interface StatusCodeResponse {
statusCode: 200 | 500;
}
export async function handler(event: APIGatewayProxyWebsocketEventV2): Promise<StatusCodeResponse> {
const { requestContext: { domainName, stage, connectionId, connectedAt, routeKey },} = event;
try {
const client = new ApiGatewayManagementApiClient({
region: process.env.AWS_REGION,
endpoint: 'https://' + domainName + '/' + stage,
});
const encoder = new TextEncoder();
const postCmd = new PostToConnectionCommand({
ConnectionId: connectionId,
Data: encoder.encode(`Hello from ${routeKey}. You connected at: ${connectedAt}`),
});
await client.send(postCmd);
} catch (err: unknown) {
return { statusCode: 500 };
}
return { statusCode: 200 };
}
I am using the AWS JS SDK v3
Add a RouteResponse in the Console ($default
route > "Add integration response" button), or with the create-route-response API, or with CloudFormation AWS::ApiGatewayV2::RouteResponse
Upvotes: 4