dotnet / dotnet/runtime

WASI runtime HttpClient header parsing bug: sentinel "-1" surfaced as header value for Azure Management responses

Open
#119,934 4 comments 0 reactions 0 assignees View on GitHub
arch-wasm area-System.Net.Http os-wasi
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

# WASI HttpClient (WasiHttpHandler) FormatException parsing `management.azure.com` response header value `-1`; Azure Management requests also return 400 *Invalid Header*

## Summary
When running a .NET 10 (preview) WASI application under `wasmtime -S http`, `HttpClient` calls to `https://management.azure.com` fail:

1. **Raw `HttpClient` GET (no Authorization)** → throws
`System.FormatException: The format of value '-1' is invalid.`
originating in `System.Net.Http.WasiHttpInterop.ConvertResponseHeaders`.

2. **Azure SDK (ArmClient) GET** (listing AVS private clouds) → server responds
`400 Bad Request - Invalid Header` even after stripping all request headers except `Authorization`.

Requests to other domains (e.g. `https://example.com`) succeed with status 200 under the same runtime, so the issue appears specific to Azure Management responses and/or their interaction with the WASI HTTP handler.

## Environment
| Item | Value |
|------|-------|
| .NET SDK | 10.0.100-rc.1.25451.107 |
| Target Framework | `net10.0` (preview) |
| Runtime Identifier | `wasi-wasm` |
| OS (host) | Windows (running wasmtime) |
| Wasmtime version | 36.0.2 |
| Wasmtime flags | `-S http` (enables `wasi:http`) |
| Command style | `wasmtime run -S http .wasm ` |

## Expected Behavior
- Raw unauthenticated request to Azure Management should yield a normal HTTP status (e.g. 401, 403, or 200) without throwing.
- Azure SDK request should fail with an auth status (401/403) if the token is invalid, not `400 Invalid Header`.
- No `FormatException` during header parsing; negative sentinel values should not surface as HTTP header values.

## Actual Behavior
- Raw request: `FormatException: The format of value '-1' is invalid.` before status / headers can be consumed.
- Azure SDK request: `400 Bad Request - Invalid Header` HTML response, even after aggressive header stripping (only `Authorization` preserved).
- Other domains behave normally (`example.com` → 200 OK).

## Minimal Raw Reproduction (Code Snippet)
```csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
static async Task Main(string[] args)
{
// Provide arg "mgmt" or set RAW_URL env var.
var mode = args.Length > 0 ? args[0] : "example";
string url = mode switch
{
"example" => "https://example.com",
"mgmt" => "https://management.azure.com/metadata/endpoints?api-version=2020-01-01",
_ => mode
};
var overrideUrl = Environment.GetEnvironmentVariable("RAW_URL");
if (!string.IsNullOrWhiteSpace(overrideUrl)) url = overrideUrl!;

using var client = new HttpClient();
var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Headers.Accept.ParseAdd("application/json");
Console.WriteLine("[REPRO] GET " + url);

try
{
var resp = await client.SendAsync(req); // Throws FormatException for mgmt endpoint
Console.WriteLine($"[REPRO] Status: {(int)resp.StatusCode} {resp.StatusCode}");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine("[REPRO] Exception: " + ex);
return 1;
}
}
}
```

## Reproduction Steps
1. Build & publish a WASI app (existing `console` project or snippet above compiled for `net10.0` with `RuntimeIdentifier=wasi-wasm`).
2. Success baseline:
```
wasmtime run -S http ./console.wasm RAW_TEST=1 RAW_URL=https://example.com dummy-sub
```
Result: `200 OK`
3. Failure (metadata endpoint):
```
wasmtime run -S http --env RAW_TEST=1 --env RAW_URL=https://management.azure.com/metadata/endpoints?api-version=2020-01-01 ./console.wasm dummy-sub
```
Result: `System.FormatException: The format of value '-1' is invalid.`
4. Azure SDK path (optional):
```
wasmtime run -S http --env AZURE_SUBSCRIPTION_ID= --env AZURE_TOKEN= ./console.wasm
```
Result: `400 Bad Request - Invalid Header`
5. Header stripping & short token (len=3) do **not** change the 400 outcome.

## Stack Trace (Representative)
```
System.FormatException: The format of value '-1' is invalid.
at System.Net.Http.Headers.HttpHeaderParser.ParseValue(String value, Object store, Int32& index)
at System.Net.Http.Headers.HttpHeaders.ParseAndAddValue(HeaderDescriptor descriptor, HttpHeaders.HeaderStoreItemInfo info, String value)
at System.Net.Http.Headers.HttpHeaders.Add(HeaderDescriptor descriptor, String value)
at System.Net.Http.Headers.HttpHeaders.Add(String name, String value)
at System.Net.Http.WasiHttpInterop.ConvertResponseHeaders(ITypes.IncomingResponse incoming, HttpResponseMessage response)
at System.Net.Http.WasiRequestWrapper.SendRequestAsync(...)
at System.Net.Http.WasiHttpHandler.SendAsync(...)
at System.Net.Http.HttpClient.<g__Core|83_0>d.MoveNext()
```

## Additional Diagnostics Attempted
| Diagnostic | Result |
|------------|--------|
| Strip all request headers except `Authorization` | 400 persists |
| Remove `Authorization` (raw test) | FormatException persists |
| Very short dummy token | Still 400 |
| Alternate domain (`example.com`) | Works (200 OK) |
| Alternate Azure path (metadata vs subscriptions) | Both fail |
| Add/remove `User-Agent`, `Accept` | No change |

## Hypothesis
`WasiHttpHandler` / `WasiHttpInterop` is mapping an internal sentinel (e.g. unknown length `-1`) directly into HTTP headers (likely `Content-Length`) causing the `FormatException`. Separately or consequently, outbound header framing to Azure Management may be malformed, yielding server-side `400 Invalid Header` responses.

## Suggested Fix Direction
1. In header conversion, if `Content-Length` parses negative (or any numeric header invalid) skip adding it instead of throwing.
2. Add optional instrumentation (env-guarded) logging raw (name,value) pairs pre-add.
3. Gracefully ignore & log (debug) unparseable headers; avoid surfacing them to application code.
4. Inspect outbound serialization for potential malformed header formatting leading to server 400.
5. Add regression tests with a mocked incoming response containing `Content-Length: -1`.

### Patch Pseudocode
```csharp
foreach (var (name, value) in incomingHeaders) {
if (name.Equals("Content-Length", StringComparison.OrdinalIgnoreCase)) {
if (!long.TryParse(value, out var len) || len < 0) {
continue; // skip invalid/sentinel length
}
}
try { response.Headers.Add(name, value); }
catch (FormatException) { /* debug log & continue */ }
}
```

## Acceptance Criteria
- No `FormatException` on management.azure.com calls.
- Raw unauthenticated call returns HTTP status (e.g., 401) instead of throwing.
- Azure SDK request fails with expected auth status (401/403) when token invalid, not 400 due to header formatting.
- Regression test prevents reintroduction of negative length header parsing failures.

## Additional Notes
If the negative value originates inside the wasi:http adapter (unknown length sentinel), correct translation should be “omit `Content-Length`” and rely on end-of-stream semantics (especially for HTTP/2-like semantics where chunked encoding isn’t used).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.