Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions lambda/api_client_proxy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 2 additions & 1 deletion lambda/extensions_api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
11 changes: 10 additions & 1 deletion lambda/runtime_api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading