Skip to content

AWF API proxy doubles https:// scheme in --anthropic-api-target URL #25137

@salekseev

Description

@salekseev

Description

When using engine.env.ANTHROPIC_BASE_URL with a GitHub Actions expression (e.g., ${{ vars.ANTHROPIC_BASE_URL }}), the AWF API proxy constructs a malformed URL by prepending https:// to a target that already includes the scheme. This only affects expression-based values — hardcoded URLs are stripped correctly at compile time.

Reproduction

Workflow .md configuration:

engine:
  id: claude
  version: "2.1.92"
  model: claude-sonnet-4-6
  env:
    ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }}

network:
  allowed:
    - defaults
    - my-custom-gateway.example.com

Where vars.ANTHROPIC_BASE_URL = https://my-custom-gateway.example.com/some-path

Compiled output

The compiler generates:

--enable-api-proxy --anthropic-api-target '${{ vars.ANTHROPIC_BASE_URL }}'

At runtime, GitHub resolves this to --anthropic-api-target 'https://my-custom-gateway.example.com/some-path'.

Expected behavior

The API proxy forwards requests to https://my-custom-gateway.example.com/some-path/v1/messages correctly.

Actual behavior

The Squid proxy logs show a request to https://https/* and returns ERR_ACCESS_DENIED (403).

Root cause

Traced through the source — this is a compile-time/runtime mismatch across three layers:

1. Compiler: pkg/workflow/awf_helpers.goextractAPITargetHost()

host := baseURL
if idx := strings.Index(host, "://"); idx != -1 {
    host = host[idx+3:]
}

At compile time, the value is the literal string ${{ vars.ANTHROPIC_BASE_URL }} — which does not contain ://. The function returns it unchanged, and the lock file emits:

--anthropic-api-target '${{ vars.ANTHROPIC_BASE_URL }}'

If the value were hardcoded (e.g., https://my-gateway.example.com), the scheme would be stripped correctly. The bug only manifests with ${{ ... }} expressions.

2. AWF CLI: src/cli.ts

At runtime, GitHub resolves the expression to the actual URL (e.g., https://my-gateway.example.com). The CLI does no scheme stripping — it passes the full URL into the container as ANTHROPIC_API_TARGET.

3. API Proxy: containers/api-proxy/server.js

The proxy reads the env var directly:

const ANTHROPIC_API_TARGET = process.env.ANTHROPIC_API_TARGET || 'api.anthropic.com';

Then uses it in two places that assume a bare hostname:

  • buildUpstreamPath(): new URL(reqUrl, "https://${targetHost}") — constructs https://https://my-gateway.example.com/... (double scheme)
  • proxyRequest(): hostname: targetHost passed to https.request() — Node treats https://my-gateway.example.com as a literal hostname, and HttpsProxyAgent sends CONNECT https://my-gateway.example.com:443 through Squid, which parses the host as https

Proposed fix

The simplest defensive fix is a one-liner in containers/api-proxy/server.js — normalize ANTHROPIC_API_TARGET to strip any scheme on startup:

- const ANTHROPIC_API_TARGET = process.env.ANTHROPIC_API_TARGET || 'api.anthropic.com';
+ const ANTHROPIC_API_TARGET = (process.env.ANTHROPIC_API_TARGET || 'api.anthropic.com').replace(/^https?:\/\//, '');

This works regardless of whether the value arrives with or without a scheme, and covers both the expression-based path and any future callers. The compiler's extractAPITargetHost() already handles hardcoded URLs correctly, so no change is needed there. An optional belt-and-suspenders strip in src/cli.ts before passing to the container would also be reasonable.

Environment

  • gh-aw v0.67.1
  • AWF image tag: 0.25.13
  • Runner: ubuntu-latest

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions