Creating an HTTP Function using a container image built with Python

For general details about how to use a container image to create and execute an HTTP function, see Creating an HTTP Function Using a Container Image and executing the Function.

This chapter introduces how to create an image using Python and perform local verification for HTTP functions.

Note

You need to implement an HTTP server in the image listening to port 8000 to receive requests.

Step 1: Create the Project

In this example we use the FastAPI framework to create an HTTP server.

For full example, see: container-http-fastapi sample in the GitHub repository.

For details about FastAPI, see FastAPI - Python web framework.

Initialize the project:

First, create a project directory:

mkdir -p my-http-function/src
cd my-http-function
python -m venv venv
source venv/bin/activate

Then, create a requirements.txt file in the project root folder and add the following content:

requirements.txt
fastapi[standard]==0.116.1
uvicorn==0.35.0
requests==2.32.3
asgi-correlation-id==4.3.4

Install the dependencies using pip:

python3 -m pip install -r requirements.txt

Implementing the function

Next, create following files:

  • src/app.py for the function entry

from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse

from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
from fastapi.middleware.cors import CORSMiddleware

import logging
import logging.config

from pydantic import BaseModel
from api.items import items_router
from asgi_correlation_id import CorrelationIdMiddleware, correlation_id
import time

from fastapi.exception_handlers import http_exception_handler
from fastapi.responses import JSONResponse

import traceback
import os

ROOT_LEVEL = os.environ.get("RUNTIME_LOG_LEVEL", "DEBUG").upper()


class UTCFormatter(logging.Formatter):
    converter = time.gmtime

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": True,
    "filters": {
        "correlation_id": {
            "()": "asgi_correlation_id.CorrelationIdFilter",
            "uuid_length": 36,
            "default_value": "-",
        },
    },
    "formatters": {
        "standard": {
            "()": UTCFormatter,
            "format": "%(asctime)s.%(msecs)03dZ [%(levelname)s] [%(correlation_id)s] %(name)s: %(message)s",
            "datefmt": "%Y-%m-%dT%H:%M:%S",
        },
    },
    "handlers": {
        "default": {
            "level": ROOT_LEVEL,
            "formatter": "standard",
            "class": "logging.StreamHandler",
            "filters": ["correlation_id"],
            "stream": "ext://sys.stdout",  # Default is stderr
        },
    },
    "loggers": {
        "": {  # root logger
            "level": ROOT_LEVEL,  # "INFO",
            "handlers": ["default"],
            "propagate": False,
        },
        "uvicorn.error": {
            "level": "INFO",
            "handlers": ["default"],
            "propagate": False,
        },
        "uvicorn.access": {
            "level": ROOT_LEVEL,
            "handlers": ["default"],
            "propagate": False,
        },
    },
}

logger = logging.getLogger(__name__)

tags_metadata = [
    {"name": "API", "description": "API samples"},
]


# see: https://fastapi.tiangolo.com/advanced/events/?h=async+conte#lifespan
@asynccontextmanager
async def lifespan(app: FastAPI):
    # startup
    logging.config.dictConfig(LOGGING_CONFIG)
    yield
    # cleanup


app = FastAPI(
    lifespan=lifespan,
    title="FunctionGraph and FastAPI",
    summary="T Cloud Public sample for FunctionGraph HTTP function using FastAPI",
    version="0.0.1",
    debug=True,
    docs_url=None,  # will be overwritten, see below: overridden_swagger()
    redoc_url=None,  # will be overwritten, see below: overridden_redoc()
    openapi_tags=tags_metadata,
)


@app.get("/docs", include_in_schema=False)
async def overridden_swagger():
    return get_swagger_ui_html(
        openapi_url=app.openapi_url, title=app.title, swagger_favicon_url="/favicon.ico"
    )


@app.get("/redoc", include_in_schema=False)
async def overridden_redoc():
    return get_redoc_html(
        openapi_url=app.openapi_url, title=app.title, redoc_favicon_url="/favicon.ico"
    )


@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
    logger.error(traceback.format_exc())

    return await http_exception_handler(
        request,
        HTTPException(
            500,
            "Internal server error",
            headers={"x-cff-request-id": correlation_id.get() or ""},
        ),
    )


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response


# add requestid to logging, see: https://github.com/snok/asgi-correlation-id
app.add_middleware(CorrelationIdMiddleware, header_name="x-cff-request-id")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=[
        # "X-Requested-With",
        # "X-Request-ID",
        # "x-cff-request-id",
        # "x-cff-security-access-key",
        # "x-cff-security-secret-key",
        # "x-cff-security-token",
        "Access-Control-Allow-Origin",
        "Access-Control-Expose-Headers",
    ],
    expose_headers=[
        # "X-Request-ID",
        # "x-cff-request-id",
        "Access-Control-Allow-Origin",
        "Access-Control-Expose-Headers",
    ],
)

@app.get("/")
async def read_root(request: Request):
    headers = request.headers
    logging.debug("Debug hello")

    logging.debug("Request headers: %s", dict(headers))
    
    ak = headers.get("x-cff-security-access-key")
    sk = headers.get("x-cff-security-secret-key")
    st = headers.get("x-cff-security-token")
    token = headers.get("x-cff-auth-token")
    
    if ak and ak != "null":
        logging.info("Access Key is: %s", ak)
    else:
        logging.error("NO AGENCY SPECIFIED OR KEYS NOT INCLUDED")
        
    return {"Hello": "World", "Agency ok": ak is not None and ak != "null", "Request ID": correlation_id.get() or ""}

@app.get("/index")
async def get_index(request: Request):
    
    logger.debug("Received GET request with query: %s", request.query_params)
    
    name = request.query_params.get("name", "unknown")
        
    return {"message": f"Hello, {name}!"}


class Message(BaseModel):
    name: str

@app.post("/index")
async def post_index(request: Request, message: Message):
  
    body = await request.body()
    logger.info("Received POST request with body: %s", body)
    
    name = message.name
        
    return {"message": f"Hello, {name}!"}
    

@app.get("/favicon.ico", include_in_schema=False)
async def favicon():
    return FileResponse(
        "./static/favicon.ico", media_type="image/x-icon", filename="favicon.ico"
    )


app.include_router(items_router, prefix="/api", tags=["API"])

if __name__ == "__main__":
    import uvicorn
    
    if os.environ.get("IS_DOCKER_CONTAINER", "0") == "1":
        # running in docker container
        uvicorn.run("app:app", host="0.0.0.0", port=8000, log_level="debug", log_config=LOGGING_CONFIG, reload=False)
    else:
        # running locally
        uvicorn.run("app:app", host="localhost", port=8000, log_level="debug", log_config=LOGGING_CONFIG, reload=True)


In this code, we create a FastAPI application that listens on port 8000.

  • src/api/items.py for the sample FastAPI router.

from fastapi import  APIRouter
from typing import Union

items_router = APIRouter()

@items_router.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

Step 2: Build the Container Image

Create a Makefile

To simplify the development and testing process, create a Makefile in the project root folder:

SHELL:=/bin/bash
TARGET_PATH=target

CURRENT_MAKEFILE := $(firstword $(MAKEFILE_LIST))

# Docker build configuration
DOCKER_FILE=Dockerfile
IMAGE_NAME=custom_container_http_fastapi_python

# Terraform backend configuration
BACKEND_CONFIG_BUCKET := "doc-samples-tf-backend"
BACKEND_CONFIG_KEY := "terraform_state/python/python-container-http-fastapi.tf"
BACKEND_CONFIG_REGION := "eu-de"
BACKEND_CONFIG_ENDPOINTS := "endpoints={s3=\"https://obs.eu-de.otc.t-systems.com\"}"


#############################################################################################
# Docker targets
#############################################################################################

build: clean

docker_build: 
  docker buildx build \
    --platform linux/amd64 \
    --file $(DOCKER_FILE) \
    --tag $(IMAGE_NAME):latest .

docker_run_local:
  docker container run \
    --rm \
    --platform linux/amd64 \
    --publish 8000:8000 \
    --user 1003:1003 \
    --name $(IMAGE_NAME) \
    $(IMAGE_NAME):latest

docker_push: docker_build
  # see: https://docs.otc.t-systems.com/software-repository-container/umn/image_management/obtaining_a_long-term_valid_login_command.html#swr-01-1000
  docker login -u $(OTC_SDK_PROJECTNAME)@$(OTC_SDK_AK) -p $(OTC_SWR_LOGIN_KEY) $(OTC_SWR_ENDPOINT)
  docker tag $(IMAGE_NAME):latest $(OTC_SWR_ENDPOINT)/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest
  docker push $(OTC_SWR_ENDPOINT)/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest

docker_all: build docker_push

clean:
  rm -rf $(TARGET_PATH)

all: build docker_build

#############################################################################################
# Terraform targets
#############################################################################################
tf_init:
  terraform -chdir=terraform \
    init \
    -backend-config=$(BACKEND_CONFIG_ENDPOINTS) \
    -backend-config="bucket=$(BACKEND_CONFIG_BUCKET)" \
    -backend-config="key=$(BACKEND_CONFIG_KEY)" \
    -backend-config="region=$(BACKEND_CONFIG_REGION)"

tf_plan: 
  if [ ! -f "terraform/.terraform.lock.hcl" ]; then \
    $(MAKE) -f $(CURRENT_MAKEFILE) tf_init; \
  fi
  terraform -chdir=terraform \
    plan \
    -var-file="variables.tfvars" \
    -var="image_url=$(OTC_SWR_ENDPOINT)/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest"

tf_apply: docker_push
  if [ ! -f "terraform/.terraform.lock.hcl" ]; then \
    $(MAKE) -f $(CURRENT_MAKEFILE) tf_init; \
  fi
  terraform -chdir=terraform \
    apply -auto-approve \
    -var-file="variables.tfvars" \
    -var="image_url=$(OTC_SWR_ENDPOINT)/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest"

tf_apply_no_push:
  if [ ! -f "terraform/.terraform.lock.hcl" ]; then \
    $(MAKE) -f $(CURRENT_MAKEFILE) tf_init; \
  fi
  terraform -chdir=terraform \
    apply -auto-approve \
    -var-file="variables.tfvars" \
    -var="image_url=$(OTC_SWR_ENDPOINT)/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest"

tf_destroy:
  terraform -chdir=terraform \
    destroy -auto-approve \
    -var-file="variables.tfvars" \
    -var="image_url=$(OTC_SWR_ENDPOINT)/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest"

#############################################################################################
# Test targets
#############################################################################################

test_local_get_index:
  # execute a curl request against the local function
  curl -X GET \
     -H 'Content-Type: application/json' \
     http://localhost:8000/index?name=John
  @echo ""

test_local_post_index:
  # execute a curl request against the local function
  curl -X POST \
     -H 'Content-Type: application/json' \
     -d @./resources/curl_apig_post_index.json \
     http://localhost:8000/index
  @echo ""

test_deployed_apig_post:
  # get the API Gateway URL from terraform output
  $(eval API_GATEWAY_TRIGGER_URL := $(shell terraform -chdir=terraform output -raw API_GATEWAY_TRIGGER_URL))
  @echo "Using API Gateway URL: $(API_GATEWAY_TRIGGER_URL)"
  # execute a curl request against the deployed function via API Gateway
  curl -X POST \
     -H 'Content-Type: application/json' \
     -d @./resources/curl_apig_post_index.json \
     $(API_GATEWAY_TRIGGER_URL)/index

test_deployed_apig_get:
  # get the API Gateway URL from terraform output
  $(eval API_GATEWAY_TRIGGER_URL := $(shell terraform -chdir=terraform output -raw API_GATEWAY_TRIGGER_URL))
  @echo "Using API Gateway URL: $(API_GATEWAY_TRIGGER_URL)"
  # execute a curl request against the deployed function via API Gateway
  curl -X GET \
     -H 'Content-Type: application/json' \
     $(API_GATEWAY_TRIGGER_URL)
  @echo "" 

test_deployed_apig_getitem:
  # get the API Gateway URL from terraform output
  $(eval API_GATEWAY_TRIGGER_URL := $(shell terraform -chdir=terraform output -raw API_GATEWAY_TRIGGER_URL))
  @echo "Using API Gateway URL: $(API_GATEWAY_TRIGGER_URL)"
  # execute a curl request against the deployed function via API Gateway
  curl -X GET \
     -H 'Content-Type: application/json' \
     $(API_GATEWAY_TRIGGER_URL)/api/items/100?q=10
  @echo "" 

.PHONY: build run_local docker_build docker_run_local docker_push docker_all test_local test_deployed clean tf_init tf_plan tf_apply tf_destroy

Create a Dockerfile

Create a Dockerfile in the project root folder to define the image.

Note

  • In the cloud environment, UID 1003 and GID 1003 are used to start the container by default.
    The two IDs can be modified by choosing Configuration > Basic Settings > Container Image Override
    on the function details page. They cannot be root or a reserved ID.
  • If the base image of the Alpine version is used, run the addgroup and adduser instead of groupadd and useradd commands.
  • You can use any base image that meets your application requirements.

Note

  • Ubuntu images are larger in size but come with more pre-installed libraries.

  • Alpine images are smaller in size but may require additional libraries depending on the application requirements.

Following example uses an Python 3.10 base image.

# syntax=docker/dockerfile:1.7
FROM python:3.10-slim@sha256:5f9928ea39771e8ddf4fb9a96ab24f65f087793635614405a1dc9384f040852e

ENV HOME=/home/paas_user
ENV PYTHONDONTWRITEBYTECODE=1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1

ENV GROUP_ID=1003
ENV GROUP_NAME=paas_user

ENV USER_ID=1003
ENV USER_NAME=paas_user

# Set timezone to UTC
ENV TZ=Etc/UTC

ENV IS_DOCKER_CONTAINER=1

RUN groupadd -g ${GROUP_ID} ${GROUP_NAME} && \
    useradd -u ${USER_ID} -g ${GROUP_ID} ${USER_NAME} && \
    mkdir -p ${HOME} && \
    chown ${USER_ID}:${GROUP_ID} ${HOME} && \
    chmod 750 ${HOME}

# Copy requirements.txt
COPY --chown=${USER_ID}:${GROUP_ID} ./requirements.txt ${HOME}/requirements.txt

RUN --mount=type=cache,target=/root/.cache/pip \
    python -m pip install --no-compile -r ${HOME}/requirements.txt

# Copy application files after dependencies for better layer caching.
COPY --chown=${USER_ID}:${GROUP_ID} ./src ${HOME}/src
COPY --chown=${USER_ID}:${GROUP_ID} ./entrypoint.sh ${HOME}/entrypoint.sh

RUN chmod 755 ${HOME}/entrypoint.sh

USER ${USER_NAME}
WORKDIR ${HOME}
EXPOSE 8000

ENTRYPOINT ["sh", "/home/paas_user/entrypoint.sh"]

Create following entrypoint script to start the server in the container:

#!/bin/sh

cd /home/paas_user/src
python3 app.py

Build and verify the image locally

1. Build the image

Build the image either using docker build or the Makefile target docker_build:

Run the following command in the project root folder to build the image:

docker buildx build \
   --platform linux/amd64 \
   --file Dockerfile \
   --tag custom_container_http_fastapi-python:latest .

2. Run the image locally

Run the image either using docker run or the Makefile target docker_run_local:

Run the following command in the project root folder to run the image:

docker container run --rm \
  --platform linux/amd64 \
  --publish 8000:8000 \
  --name custom_container_http_fastapi-python \
  custom_container_http_fastapi-python:latest

3. Test the image locally

Test the image either using curl or the Makefile target test_local:

Run the following command in a new terminal to test the image using a curl command:

curl -X POST -H 'Content-Type: application/json' -d '{"name":"T Cloud Public"}' localhost:8000/index

You should see output similar to the following:

{"message":"Hello, T Cloud Public!"}

Step 3: Upload the Container Image to SWR (SoftWare Repository for Container)

For details on SWR (SoftWare Repository for Container), see:

Prerequisites

  • SWR instance created.

  • Credentials for SWR created.

Upload the image to SWR

To upload the container image to SWR, following values are needed:

Parameter

Description

OTC_SDK_PROJECTNAME

Your project name.
To obtain this, see: Obtaining a Project ID in API usage guide but use the project name instead of the project ID.

OTC_SDK_AK

Your Access Key

OTC_SWR_LOGIN_KEY

The login key for SWR.
For details see: Obtaining a Long-Term Docker Login Command in the Software Repository for Container user manual.

It can be generated using the access key ${OTC_SDK_AK} and secret key ${OTC_SDK_SK} as follows:
export OTC_SWR_LOGIN_KEY=$(printf "${OTC_SDK_AK}" | \
        openssl dgst -binary -sha256 -hmac "${OTC_SDK_SK}" | \
        od -An -vtx1 | sed 's/[ \n]//g' | sed 'N;s/\n//')

OTC_SWR_ENDPOINT

SWR endpoint, e.g. swr.eu-de.otc.t-systems.com

OTC_SWR_ORGANIZATION

Your SWR organization name

IMAGE_NAME

The name of your container image

Set the environment variables:
export OTC_SDK_PROJECTNAME=<your_project_name>
export OTC_SDK_AK=<your_access_key>
export OTC_SDK_SK=<your_secret_key>
export OTC_SWR_LOGIN_KEY=$(printf "${OTC_SDK_AK}" | \
        openssl dgst -binary -sha256 -hmac "${OTC_SDK_SK}" | \
        od -An -vtx1 | sed 's/[ \n]//g' | sed 'N;s/\n//')
export OTC_SWR_ENDPOINT=swr.eu-de.otc.t-systems.com
export OTC_SWR_ORGANIZATION=<your_swr_organization>
export IMAGE_NAME=custom_container_event_example

Upload the image to SWR either using shell commands or the Makefile target docker_push:

Run the following commands in the container-event folder to upload the image to SWR:

1. Login to SWR
  docker login -u ${OTC_SDK_PROJECTNAME}@${OTC_SDK_AK} -p ${OTC_SWR_LOGIN_KEY} ${OTC_SWR_ENDPOINT}
2. Tag the image
  docker tag ${IMAGE_NAME}:latest ${OTC_SWR_ENDPOINT}/${OTC_SWR_ORGANIZATION}/${IMAGE_NAME}:latest
3. Push the image to SWR
  docker push ${OTC_SWR_ENDPOINT}/${OTC_SWR_ORGANIZATION}/${IMAGE_NAME}:latest

Step 4: Create an HTTP Function Using the Container Image

  1. In the left navigation pane of the management console, choose Compute > FunctionGraph. On the FunctionGraph console, choose Functions > Function List from the navigation pane.

  2. Click Create Function in the upper right corner. On the displayed page, select Container Image for creation mode.

  3. Set the basic function information.

    • Function Type: Select HTTP Function.

    • Region: The default value is used. You can select other regions.

      Regions are geographic areas isolated from each other. Resources are region-specific and cannot be used across regions through internal network connections. For low network latency and quick resource access, select the nearest region.

    • Function Name: Enter e.g. custom_container_http.

    • Enterprise Project: The default value is default. You can select the created enterprise project.

      Enterprise projects let you manage cloud resources and users by project.

    • Agency: Select an agency with the SWR Admin permission. If no agency is available, create one by referring to Creating an Agency.

    • Container Image: Enter the image uploaded to SWR. The format is: {SWR_endpoint}/{organization_name}/{image_name}:{tag}.

      Example: swr.eu-de.otc.t-systems.com/my_organization/custom_container_http_example:latest.

  4. Advanced Settings: Collect Logs is disabled by default. If it is enabled, function execution logs will be reported to Log Tank Service (LTS). You will be billed for log management on a pay-per-use basis.

    Parameter

    Description

    Log Configuration

    You can select Auto or Custom.

    • Auto: Use the default log group and log stream. Log groups prefixed with “functiongraph.log.group” are filtered out.

    • Custom: Select a custom log group and log stream. Log streams that are in the same enterprise project as your function.

    Log Tag

    You can use these tags to filter function logs in LTS.
    You can add 10 more tags.
    Tag key/value: Enter a maximum of 64 characters.
    Only digits, letters, underscores (_), and hyphens (-) are allowed.
  5. After the configuration is complete, click Create Function.

See also: Step 4: Creating Function in the user manual.

Step 5: Test the HTTP Function

On the function details page, click Test. In the displayed dialog box, create following test events.

See also: Step 5: Testing the Function in the user manual.

Test event for GET /index

  • set Event Name to get_index,

  • modify the test event as follows,

    Test event for GET /index
    {
      "body": "",
      "requestContext": {
          "apiId": "bc1dcffd-aa35-474d-897c-d53425a4c08e",
          "requestId": "11cdcdcf33949dc6d722640a13091c77",
          "stage": "RELEASE"
      },
      "queryStringParameters": {
          "responseType": "application/json",
          "name": "John"
      },
      "httpMethod": "GET",
      "pathParameters": {
      },
      "headers": {
          "accept-language": "q=0.5,en-US;q=0.3,en;q=0.2",
          "accept-encoding": "gzip, deflate, br",
          "x-forwarded-port": "443",
          "x-forwarded-for": "103.218.216.98",
          "accept": "application/json",
          "upgrade-insecure-requests": "1",
          "host": "host",
          "x-forwarded-proto": "https",
          "pragma": "no-cache",
          "cache-control": "no-cache",
          "x-real-ip": "103.218.216.98",
          "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0"
      },
      "path": "/index",
      "isBase64Encoded": false
    }
    
  • and click Create.

Test event for POST /index

  • set Event Name to post_index,

  • modify the test event as follows,

    Test event for POST /index
    {
      "body": "{\"name\": \"Hello World\"}",
      "requestContext": {
          "apiId": "bc1dcffd-aa35-474d-897c-d53425a4c08e",
          "requestId": "11cdcdcf33949dc6d722640a13091c77",
          "stage": "RELEASE"
      },
      "queryStringParameters": {
          "responseType": "application/json"
      },
      "httpMethod": "POST",
      "pathParameters": {},
      "headers": {
          "content-type": "application/json; charset=utf-8",
          "accept-language": "q=0.5,en-US;q=0.3,en;q=0.2",
          "accept-encoding": "gzip, deflate, br",
          "x-forwarded-port": "443",
          "x-forwarded-for": "103.218.216.98",
          "accept": "application/json",
          "upgrade-insecure-requests": "1",
          "host": "host",
          "x-forwarded-proto": "https",
          "pragma": "no-cache",
          "cache-control": "no-cache",
          "x-real-ip": "103.218.216.98",
          "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0"
      },
      "path": "/index",
      "isBase64Encoded": false
    }
    
  • and click Create.

Step 6: View the Execution Result

Click Test to execute the function and you should see the following output in the Execution Result section on the right for post_index test event:

{
  "body": "eyJtZXNzYWdlIjoiSGVsbG8sIEhlbGxvIFdvcmxkISJ9",
  "headers": {
    "Content-Length": [
      "33"
    ],
    "Content-Type": [
      "application/json; charset=utf-8"
    ],
    "Date": [
      "Thu, 19 Mar 2026 11:03:46 GMT"
    ]
  },
  "statusCode": 200,
  "isBase64Encoded": true
}

The response body is Base64-encoded. After decoding, you will get the string “Hello World!”.

For get_index test event the output should be:

{
  "body": "eyJtZXNzYWdlIjoiSGVsbG8sIEpvaG4hIn0=",
  "headers": {
    "Content-Length": [
      "26"
    ],
    "Content-Type": [
      "application/json; charset=utf-8"
    ],
    "Date": [
      "Thu, 19 Mar 2026 11:03:46 GMT"
    ]
  },
  "statusCode": 200,
  "isBase64Encoded": true
}

After decoding, you will get the string “Hello World, John!”.