From 32e2e98dd799a2fbe7c540834f051aab400b994e Mon Sep 17 00:00:00 2001 From: ilyasse benrkia Date: Thu, 17 Sep 2026 15:20:17 +0000 Subject: [PATCH] fix: bypass proxy for Runtime and Extensions API calls The Runtime and Extensions API clients built a bare http.Client, which inherits http.DefaultTransport and its ProxyFromEnvironment behavior. A customer-configured HTTP(S)_PROXY then routed calls to the API endpoint through the proxy, so a proxy in the environment could break function init even though it should never affect communication with the API. Give both clients a transport that never proxies, cloned from the default so connection-pooling and timeout defaults are preserved. --- lambda/api_client_proxy_test.go | 63 +++++++++++++++++++++++++++++++++ lambda/extensions_api_client.go | 3 +- lambda/runtime_api_client.go | 11 +++++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 lambda/api_client_proxy_test.go diff --git a/lambda/api_client_proxy_test.go b/lambda/api_client_proxy_test.go new file mode 100644 index 00000000..ec4230f8 --- /dev/null +++ b/lambda/api_client_proxy_test.go @@ -0,0 +1,63 @@ +// Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved + +package lambda + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +// These tests verify both API clients bypass any configured proxy when +// reaching the link-local Runtime/Extensions API. A non-loopback URL is used +// because Go never proxies loopback addresses, which would mask the bug. + +func nonLoopbackAPIRequest(t *testing.T) *http.Request { + t.Helper() + req, err := http.NewRequest(http.MethodGet, "http://192.0.2.1:9001/2018-06-01/runtime/invocation/next", nil) + require.NoError(t, err) + return req +} + +func proxyForClient(t *testing.T, client *http.Client, req *http.Request) *string { + t.Helper() + rt := client.Transport + if rt == nil { + rt = http.DefaultTransport + } + transport, ok := rt.(*http.Transport) + require.True(t, ok, "expected an *http.Transport to inspect proxy behavior") + if transport.Proxy == nil { + return nil + } + proxyURL, err := transport.Proxy(req) + require.NoError(t, err) + if proxyURL == nil { + return nil + } + s := proxyURL.String() + return &s +} + +func TestRuntimeAPIClientBypassesProxy(t *testing.T) { + t.Setenv("HTTP_PROXY", "http://192.0.2.100:3128") + t.Setenv("HTTPS_PROXY", "http://192.0.2.100:3128") + t.Setenv("http_proxy", "http://192.0.2.100:3128") + t.Setenv("https_proxy", "http://192.0.2.100:3128") + + client := newRuntimeAPIClient("192.0.2.1:9001").httpClient + proxy := proxyForClient(t, client, nonLoopbackAPIRequest(t)) + require.Nil(t, proxy, "Runtime API client must not route through the configured proxy, got %v", proxy) +} + +func TestExtensionAPIClientBypassesProxy(t *testing.T) { + t.Setenv("HTTP_PROXY", "http://192.0.2.100:3128") + t.Setenv("HTTPS_PROXY", "http://192.0.2.100:3128") + t.Setenv("http_proxy", "http://192.0.2.100:3128") + t.Setenv("https_proxy", "http://192.0.2.100:3128") + + client := newExtensionAPIClient("192.0.2.1:9001").httpClient + proxy := proxyForClient(t, client, nonLoopbackAPIRequest(t)) + require.Nil(t, proxy, "Extensions API client must not route through the configured proxy, got %v", proxy) +} diff --git a/lambda/extensions_api_client.go b/lambda/extensions_api_client.go index 1e8b7618..cd45c93b 100644 --- a/lambda/extensions_api_client.go +++ b/lambda/extensions_api_client.go @@ -28,7 +28,8 @@ type extensionAPIClient struct { func newExtensionAPIClient(address string) *extensionAPIClient { client := &http.Client{ - Timeout: 0, // connections to the extensions API are never expected to time out + Timeout: 0, // connections to the extensions API are never expected to time out + Transport: newAPITransport(), } endpoint := "http://" + address + "/" + extensionAPIVersion + "/extension/" return &extensionAPIClient{ diff --git a/lambda/runtime_api_client.go b/lambda/runtime_api_client.go index 7607e49f..42fe5232 100644 --- a/lambda/runtime_api_client.go +++ b/lambda/runtime_api_client.go @@ -40,9 +40,18 @@ type runtimeAPIClient struct { pool *sync.Pool } +// newAPITransport returns an HTTP transport that never proxies, so calls to the +// link-local Runtime/Extensions API bypass any customer-configured proxy. +func newAPITransport() *http.Transport { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = nil + return transport +} + func newRuntimeAPIClient(address string) *runtimeAPIClient { client := &http.Client{ - Timeout: 0, // connections to the runtime API are never expected to time out + Timeout: 0, // connections to the runtime API are never expected to time out + Transport: newAPITransport(), } endpoint := "http://" + address + "/" + apiVersion + "/runtime/invocation/" userAgent := "aws-lambda-go/" + runtime.Version()