#4839·boto3

CRT-based S3 transfer manager ignores client's custom endpoint_url, silently routes to the real AWS regional endpoint

Author: RoelantStegmannCreated Sep 9, 2026Updated Sep 9, 2026
Labelsbugneeds-triage

Describe the bug

TransferConfig(preferred_transfer_client="crt") silently ignores a client's custom endpoint_url. The CRT-backed transfer manager always builds its own S3 client from region_name alone and hardcodes endpoint_url: None, so any transfer routed through it goes to the standard AWS regional endpoint instead of the endpoint the caller configured — with no error or warning. For a non-AWS endpoint (MinIO, moto, LocalStack) this means the request goes to real AWS instead of the intended target; for a VPC/FIPS endpoint it silently drops back to the public regional endpoint instead of the private/compliant one.

This is in contrast to the classic (non-CRT) s3transfer manager, which does honor client.meta.endpoint_url correctly.

Regression Issue

  • Select this option if this issue appears to be a regression.

Expected Behavior

One of:

CRT-backed transfers should honor the source client's endpoint_url, the same way the classic transfer manager does

OR

create_transfer_manager/_should_use_crt should detect a non-default endpoint_url on the client and refuse to engage CRT (falling back to the classic manager, ideally with a debug/warning log), rather than silently substituting the wrong endpoint.

Current Behavior

Reproduction Steps Confirmed over the network, not just by reading the source. Ran a local moto S3 server (python -m moto.server -p 8123) and pointed both a classic-engine and a CRT-engine transfer manager at it, with the same dummy, non-existent AWS credentials for both:

import tempfile
import boto3
from boto3.s3.transfer import TransferConfig, create_transfer_manager

ENDPOINT = "http://127.0.0.1:8123"
BUCKET = "crt-endpoint-repro"

client = boto3.client(
    "s3", endpoint_url=ENDPOINT,
    aws_access_key_id="test", aws_secret_access_key="test",
    region_name="us-east-1",
)
client.create_bucket(Bucket=BUCKET)

tmp = tempfile.NamedTemporaryFile(delete=False)
tmp.write(b"hello-transfer-manager")
tmp.close()

# Control: classic engine, same local endpoint.
with create_transfer_manager(client, TransferConfig(preferred_transfer_client="classic")) as manager:
    manager.upload(tmp.name, BUCKET, "classic-key.txt").result()
print("CONTROL (classic engine, local endpoint): upload succeeded")

# The actual test: CRT engine, same local endpoint.
with create_transfer_manager(client, TransferConfig(preferred_transfer_client="crt")) as manager:
    manager.upload(tmp.name, BUCKET, "crt-key.txt").result()

Result:

moto bucket created OK via classic client - sanity check passed CONTROL (classic engine, local endpoint): upload succeeded Attempting CRT-engine upload against http://127.0.0.1:8123 ... CRT upload FAILED after 1.1s: ClientError: An error occurred (InvalidAccessKeyId) when calling the PutObject operation: The AWS Access Key Id you provided does not exist in our records. InvalidAccessKeyId is a genuine AWS API response — moto never validates credentials at all, so moto itself could not have produced this error. moto's own request log confirms the CRT request never arrived there in the first place:

127.0.0.1 - - [.../Sep/2026 ...] "PUT /crt-endpoint-repro HTTP/1.1" 200 - 127.0.0.1 - - [.../Sep/2026 ...] "PUT /crt-endpoint-repro/classic-key.txt HTTP/1.1" 200 - (only the bucket-creation and classic-engine PUT appear — no entry at all for the CRT-engine upload attempt)

So the CRT-engine upload silently left the machine and hit real AWS instead of the configured local endpoint, exactly as the source trace below predicts. Versions used for this reproduction: boto3==1.42.59, awscrt==0.31.2, moto==5.x (server mode), on Windows 11 / Python 3.13.12, no real AWS credentials configured anywhere on the machine other than the dummy test/test pair passed explicitly to the client above.

Possible Solution

Pass client.meta.endpoint_url through to _create_crt_request_serializer (in place of the hardcoded None) and to create_s3_crt_client/_create_crt_client, mirroring how the classic transfer manager derives its endpoint from the client. If CRT's underlying create_s3_crt_client can't accept a custom endpoint at all, then _should_use_crt should treat a non-default client.meta.endpoint_url as a reason to opt out of CRT (log at debug/warning level) instead of silently proceeding.

Additional Information/Context

Found this while auditing an internal wrapper around boto3 that supports both real AWS and local/mocked S3 endpoints for testing — a lingering local dev/test setup with awscrt installed would have transfers silently misrouted to production AWS instead of the local mock, which is a fairly dangerous failure mode: no exception (the request succeeds, just against the wrong host), no log, nothing distinguishing it from a correct run except the destination. Confirmed this is not just a theoretical read of the source but an actual observed behavior — see Reproduction Steps above.

boto3==1.42.59, botocore==1.42.59, s3transfer==0.16.0 Windows 11, Python 3.13.12

Current Behavior

boto3/crt.py never threads the calling client's endpoint_url into the CRT client it constructs:

def _create_crt_request_serializer(session, region_name):
    return BotocoreCRTRequestSerializer(
        session, {'region_name': region_name, 'endpoint_url': None}
    )


def _create_crt_client(session, config, region_name, cred_provider):
    create_crt_client_kwargs = {
        'region': region_name,
        'use_ssl': True,
        'crt_credentials_provider': cred_provider,
    }
    return create_s3_crt_client(**create_crt_client_kwargs)

endpoint_url is hardcoded to None in the serializer, and _create_crt_client doesn't accept an endpoint_url parameter at all — only region. _should_use_crt (in boto3/s3/transfer.py) also has no awareness of the client's endpoint_url; it decides purely from HAS_CRT/preferred_transfer_client/instance-optimization.

Net effect: boto3.s3.transfer.create_transfer_manager(client, config) with config.preferred_transfer_client == "crt" produces a transfer manager that ignores client.meta.endpoint_url completely.

Reproduction Steps

Confirmed over the network, not just by reading the source. Ran a local moto S3 server (python -m moto.server -p 8123) and pointed both a classic-engine and a CRT-engine transfer manager at it, with the same dummy, non-existent AWS credentials for both:

import tempfile
import boto3
from boto3.s3.transfer import TransferConfig, create_transfer_manager

ENDPOINT = "http://127.0.0.1:8123"
BUCKET = "crt-endpoint-repro"

client = boto3.client(
    "s3", endpoint_url=ENDPOINT,
    aws_access_key_id="test", aws_secret_access_key="test",
    region_name="us-east-1",
)
client.create_bucket(Bucket=BUCKET)

tmp = tempfile.NamedTemporaryFile(delete=False)
tmp.write(b"hello-transfer-manager")
tmp.close()

# Control: classic engine, same local endpoint.
with create_transfer_manager(client, TransferConfig(preferred_transfer_client="classic")) as manager:
    manager.upload(tmp.name, BUCKET, "classic-key.txt").result()
print("CONTROL (classic engine, local endpoint): upload succeeded")

# The actual test: CRT engine, same local endpoint.
with create_transfer_manager(client, TransferConfig(preferred_transfer_client="crt")) as manager:
    manager.upload(tmp.name, BUCKET, "crt-key.txt").result()

Result:

moto bucket created OK via classic client - sanity check passed CONTROL (classic engine, local endpoint): upload succeeded Attempting CRT-engine upload against http://127.0.0.1:8123 ... CRT upload FAILED after 1.1s: ClientError: An error occurred (InvalidAccessKeyId) when calling the PutObject operation: The AWS Access Key Id you provided does not exist in our records. InvalidAccessKeyId is a genuine AWS API response — moto never validates credentials at all, so moto itself could not have produced this error. moto's own request log confirms the CRT request never arrived there in the first place:

127.0.0.1 - - [.../Sep/2026 ...] "PUT /crt-endpoint-repro HTTP/1.1" 200 - 127.0.0.1 - - [.../Sep/2026 ...] "PUT /crt-endpoint-repro/classic-key.txt HTTP/1.1" 200 - (only the bucket-creation and classic-engine PUT appear — no entry at all for the CRT-engine upload attempt)

So the CRT-engine upload silently left the machine and hit real AWS instead of the configured local endpoint, exactly as the source trace below predicts. Versions used for this reproduction: boto3==1.42.59, awscrt==0.31.2, moto==5.x (server mode), on Windows 11 / Python 3.13.12, no real AWS credentials configured anywhere on the machine other than the dummy test/test pair passed explicitly to the client above.

Possible Solution

Pass client.meta.endpoint_url through to _create_crt_request_serializer (in place of the hardcoded None) and to create_s3_crt_client/_create_crt_client, mirroring how the classic transfer manager derives its endpoint from the client. If CRT's underlying create_s3_crt_client can't accept a custom endpoint at all, then _should_use_crt should treat a non-default client.meta.endpoint_url as a reason to opt out of CRT (log at debug/warning level) instead of silently proceeding.

Additional Information/Context

Found this while auditing an internal wrapper around boto3 that supports both real AWS and local/mocked S3 endpoints for testing — a lingering local dev/test setup with awscrt installed would have transfers silently misrouted to production AWS instead of the local mock, which is a fairly dangerous failure mode: no exception (the request succeeds, just against the wrong host), no log, nothing distinguishing it from a correct run except the destination. Confirmed this is not just a theoretical read of the source but an actual observed behavior — see Reproduction Steps above.

SDK version used

boto3==1.42.59, botocore==1.42.59, s3transfer==0.16.0

Environment details (OS name and version, etc.)

Windows 11, Python 3.13.12