CRT-based S3 transfer manager ignores client's custom endpoint_url, silently routes to the real AWS regional endpoint
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
Source: boto/boto3