Skip to content

Requests and responses

View as Markdown

In the Python and Node.js HTTP runtimes, a function handler receives each HTTP request as an event and returns a response. The FastAPI, Flask, and Express runtimes use their own app and routes instead. See FastAPI, Flask, and Express runtimes.

In a Python HTTP runtime, define handler in func.py. In a Node.js HTTP runtime, export handler from index.mjs.

import json
async def handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({"message": "Hello World"})
}
export const handler = async (event, context) => {
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Hello World" }),
};
};

The Python handler can be an async def or a plain def. The platform calls either kind. Use async def for work that waits on I/O, such as database calls, HTTP requests, and external APIs. Use a plain def for lightweight processing and CPU-bound logic.

Go and C# have their own fixed handler. In Go, handle.go is in package main and defines this function:

func Handle(ctx context.Context, request APIGatewayProxyRequest) (APIGatewayProxyResponse, error)

In C#, Handler.cs defines a public class named Handler with this method:

public async Task<Dictionary<string, object>> HandleAsync(Dictionary<string, object> awsEvent, Dictionary<string, object> context)

In PHP, index.php runs for every request. It reads the request from PHP’s $_SERVER array and writes the response with PHP’s header and echo. The PHP sample puts that work in a function named handleRequest and calls it.

In the Python and Node.js HTTP runtimes, every request becomes an event passed to the handler. The event holds the fields below.

Field What it holds
httpMethod The HTTP method, such as GET or POST.
path The URL path, for example /users/42.
headers The request headers as key-value pairs, with lowercase names.
multiValueHeaders The request headers, each value as a list, with lowercase names.
queryStringParameters The query parameters. For a parameter given more than once, the last value. Null when there is no query string.
multiValueQueryStringParameters The query parameters, each value as a list, repeated or not. Null when there is no query string.
body The request body, as a UTF-8 string for text types or a base64 string for the others. Null when there is no body.
isBase64Encoded True when the body is base64. False for text.
rawBody The raw bytes of the request body, for parsing binary content.
requestContext An object with stage, set to local, and identity, holding sourceIp and userAgent.

Reading the event in Python:

import json
async def handler(event, context):
body = json.loads(event["body"]) if event["body"] else {}
page = (event["queryStringParameters"] or {}).get("page")
method = event["httpMethod"]
return {"statusCode": 200}

Reading the event in Node.js:

export const handler = async (event) => {
const body = event.body ? JSON.parse(event.body) : {};
const page = event.queryStringParameters?.page ?? "1";
const method = event.httpMethod;
const path = event.path;
const ip = event.requestContext.identity.sourceIp;
return { statusCode: 200 };
};

The handler’s context holds functionName, set to function, and functionVersion, set to $LATEST. To set your own stage, function name, or version, add STAGE, FUNCTION_NAME, or FUNCTION_VERSION as environment variables. The event’s stage and the context then show your values.

A handler returns a response object. Only the status code is required. To redirect, return a 302 with a Location header. To return JSON, put it in the body and set the matching Content-Type.

Field What it does
statusCode Required, an integer from 100 to 599.
headers Sets single-value headers, for example Content-Type.
multiValueHeaders Sets headers that repeat, for example Set-Cookie.
body The payload. A string, an object, or null.
isBase64Encoded Set true to send binary content.

To report an error to the caller, return a non-2xx statusCode with a body that describes it.

Some common responses in Python, with a handler that returns one of them:

import json
json_response = {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"success": True})
}
plain_text = {
"statusCode": 200,
"headers": {"Content-Type": "text/plain"},
"body": "Hello"
}
redirect = {
"statusCode": 302,
"headers": {"Location": "https://example.com"}
}
cookies = {
"statusCode": 200,
"multiValueHeaders": {"Set-Cookie": ["session=abc; HttpOnly"]},
"body": json.dumps({"ok": True})
}
no_content = {"statusCode": 204, "body": None}
def handler(event, context):
return json_response

The same responses in Node.js:

const jsonResponse = {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ success: true }),
};
const plainText = {
statusCode: 200,
headers: { "Content-Type": "text/plain" },
body: "Hello",
};
const redirect = {
statusCode: 302,
headers: { Location: "https://example.com" },
};
const cookies = {
statusCode: 200,
multiValueHeaders: { "Set-Cookie": ["session=abc; HttpOnly"] },
body: JSON.stringify({ ok: true }),
};
const noContent = { statusCode: 204, body: null };
export const handler = async (event) => jsonResponse;

To send a file, set the Content-Type and Content-Disposition headers, put the base64 bytes in the body, and set isBase64Encoded to true:

import base64
def handler(event, context):
data = b"\x89PNG\r\n\x1a\n" # a PNG's first bytes, in place of real file data
return {
"statusCode": 200,
"headers": {
"Content-Type": "image/png",
"Content-Disposition": 'inline; filename="photo.png"'
},
"body": base64.b64encode(data).decode(),
"isBase64Encoded": True
}
export const handler = async (event) => {
const data = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // a PNG's first bytes
return {
statusCode: 200,
headers: {
"Content-Type": "image/png",
"Content-Disposition": 'inline; filename="photo.png"',
},
body: data.toString("base64"),
isBase64Encoded: true,
};
};

Change CORS and body limits with the init function

Section titled “Change CORS and body limits with the init function”

Define an init function to change CORS and body handling. It runs when the function starts. Without one, the defaults apply.

def init():
return {
"cors": {
"origins": ["*"],
"credentials": False
},
"body": {
"json": True,
"urlencoded": False,
"content_types": [],
"max_bytes": 10 * 1024 * 1024
}
}
export const init = () => ({
cors: {
origins: ["*"],
credentials: false,
},
body: {
json: true,
urlencoded: false,
contentTypes: [],
maxBytes: 10 * 1024 * 1024,
},
});

The init function must be synchronous and return a plain dictionary or object. An asynchronous init function, a return value of another type, or an empty or non-string origins list stops the function from starting. The runtime logs then show an error line that says why, such as “init() must be synchronous”. See View logs and monitoring.

The defaults when the init function is not defined:

Setting Default Meaning
cors.origins ["*"] Any origin can call the function.
cors.credentials False CORS does not allow credentials.
body.json True JSON requests are accepted.
body.urlencoded False Form submissions are rejected with 415.
body.content_types [] No additional content types are accepted.
body.max_bytes 10 MB Larger bodies get 413.

By default a function answers cross-origin requests from any origin. CORS applies only to calls from a browser. Calls from a server, such as curl or another backend, reach the function whatever the origins setting.

In Node.js, two settings are spelled differently: contentTypes for content_types, and maxBytes for max_bytes.

The setting that reaches each goal:

Goal Setting
Use the defaults Do not define the init function.
Allow only some origins Set cors.origins to a list of your domains.
Allow cookies or credentials Set cors.credentials to True, and list specific domains in cors.origins.
Accept form posts Set body.urlencoded to True.
Accept file uploads Add a multipart/form-data match to body.content_types, and raise body.max_bytes if needed.
Accept XML Add text/xml and application/xml to body.content_types.
Stop accepting JSON Set body.json to False.
Accept plain text Add text/plain to body.content_types.
Accept images Add an image/ match to body.content_types.
Accept PDF Add application/pdf to body.content_types.
Accept other binary data Add application/octet-stream to body.content_types.
Set a strict body limit Set body.max_bytes below the 10 MB default.

Each entry in body.content_types is a string or a pattern. A string matches any content type that starts with it, in any case. A pattern is a compiled re pattern in Python, or a RegExp in Node.js.

Credentials need specific origins. A wildcard origin with credentials stops the function from starting.

The body size limit is 10 MB by default, and you can raise it up to 1000 MB. A value above that cap stops the function from starting. A body over the limit you set gets 413.

File upload as form data, with the limit raised to 50 MB:

import re
def init():
return {
"body": {
"content_types": [re.compile(r"^multipart/form-data")],
"max_bytes": 50 * 1024 * 1024
}
}

XML:

def init():
return {
"body": {
"content_types": ["text/xml", "application/xml"]
}
}

JSON stays accepted. A text/xml body arrives as text. An application/xml body arrives base64-encoded, with its bytes in rawBody. To accept only XML, also set “json”: False in body.

Images:

import re
def init():
return {"body": {"content_types": [re.compile(r"^image/")]}}

PDF:

def init():
return {"body": {"content_types": ["application/pdf"]}}

You can read the allowed origins from an environment variable. Set the variable in Function Configuration, and use it in the init function:

export const init = () => ({
cors: {
origins: (process.env.CORS_ORIGINS || "*").split(","),
credentials: false,
},
});

Set CORS_ORIGINS to a comma-separated list of domains, such as https://app.example.com,https://staging.example.com.

To read a form post, accept form submissions and parse the body:

export const init = () => ({
body: { json: true, urlencoded: true },
});
export const handler = async (event) => {
const form = Object.fromEntries(new URLSearchParams(event.body));
return { statusCode: 200, body: JSON.stringify({ email: form.email }) };
};

To read a file upload, accept multipart, raise the limit to 50 MB, and parse the raw body. This example uses the busboy library. Add busboy to the dependencies in your package.json:

import busboy from "busboy";
import { Readable } from "stream";
export const init = () => ({
body: {
contentTypes: [/^multipart\/form-data/],
maxBytes: 50 * 1024 * 1024,
},
});
export const handler = async (event) => {
const files = [];
await new Promise((resolve, reject) => {
const parser = busboy({ headers: event.headers });
parser.on("file", (name, file, info) => {
files.push(info.filename);
file.resume();
});
parser.on("close", resolve);
parser.on("error", reject);
Readable.from([event.rawBody]).pipe(parser);
});
return { statusCode: 200, body: JSON.stringify({ files }) };
};

To read an image, accept image types and use the raw bytes:

export const init = () => ({
body: { contentTypes: [/^image\//] },
});
export const handler = async (event) => {
// event.body holds the image as base64, and event.rawBody holds its bytes.
const bytes = event.rawBody;
return { statusCode: 200, body: JSON.stringify({ size: bytes.length }) };
};

JSON, form-urlencoded, and text/* bodies arrive in body as a UTF-8 string, and so does a body with no content type. Every other content type the init function allows arrives base64-encoded in body. The raw bytes are in rawBody. Use rawBody for multipart and file parsing.

Content type How it arrives in body isBase64Encoded
application/json UTF-8 string false
application/x-www-form-urlencoded UTF-8 string false
text/plain, text/xml, and other text/* types UTF-8 string false
No content type UTF-8 string false
application/xml base64 string true
multipart/form-data base64 string true
image/*, application/pdf, application/octet-stream base64 string true

A request with no body, such as a GET, has body set to null.

In the FastAPI, Flask, and Express runtimes, you write an ordinary app with its own routes. The app takes the place of the handler, the event, and the init function.

In these runtimes, your app decides what a request can carry, including its size and content type. The platform’s templates for these runtimes allow cross-origin calls from any origin.

For FastAPI and Flask, func.py must create the app under the name app. The platform starts it on port 8080.

For Express, index.js must start the app on the port the platform gives it, with this line:

app.listen(process.env.PORT);

The template marks that line as one not to change. Keep it as it is.

If a handler throws, the platform returns a generic error and logs the handler error. An out-of-memory error returns 503. Any other handler error returns 500. A handler that returns the wrong shape also gets 500. The caller never sees the handler’s error text.

A body larger than the configured limit returns 413 before the handler runs. A content type that is not allowed returns 415 before the handler runs.

Handle errors in the handler with a try/except, or try/catch:

import json
async def handler(event, context):
try:
body = json.loads(event["body"]) if event["body"] else {}
if not body.get("email"):
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "email required"})
}
return {"statusCode": 200, "body": json.dumps({"ok": True})}
except Exception as err:
print(err)
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "Internal Server Error"})
}
export const handler = async (event) => {
try {
const body = event.body ? JSON.parse(event.body) : {};
if (!body.email) {
return {
statusCode: 400,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ error: "email required" }),
};
}
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
} catch (err) {
console.error(err);
return {
statusCode: 500,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ error: "Internal Server Error" }),
};
}
};

See Call a function to call a function, and View logs and monitoring to read what a handler logged.