I am seeing a strange behavior while authenticating with the Xray Cloud API from a C# backend application.
Below is the code I initially used for authentication:
private async Task SetAuthHeaderAsync(HttpClient client, string clientId, string clientSecret) { var authTokenRequest = new JObject { ["client_id"] = clientId, ["client_secret"] = clientSecret, }; var httpResponse = await client.PostAsync( "authenticate", CreateHttpContent(authTokenRequest)); if (httpResponse.StatusCode == HttpStatusCode.OK) { var authToken = await DeserializeResponseAsync<string>(httpResponse); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( XRayConnectorConstants.BearerAuthScheme, authToken); return; } var message = httpResponse.StatusCode == HttpStatusCode.Unauthorized ? ErrorCodes.Validation_XRayConnector_Client_Id_Or_Secret_Incorrect : ErrorCodes.Validation_XRayConnector_Connection_Not_Reachable; _logger.LogError( "Xray authentication failed with StatusCode: {statusCode} and having Response: {response}", httpResponse.StatusCode, await httpResponse.Content.ReadAsStringAsync()); throw new HttpClientApiException(message, httpResponse.StatusCode); }private HttpContent CreateHttpContent(object content) { HttpContent httpContent = null; if (content != null) { var ms = new MemoryStream(); SerializeJsonIntoStream(content, ms); ms.Seek(0, SeekOrigin.Begin); httpContent = new StreamContent(ms); httpContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Json); } return httpContent; }
Calling:https://xray.cloud.getxray.app/api/v2/
returns 401 Unauthorized
Calling:https://eu.xray.cloud.getxray.app/api/v2/
returns 200 OK
So initially I assumed this was related to Xray data residency.
However, things get interesting here:
Calling the global endpoint (https://xray.cloud.getxray.app/api/v2/) works perfectly from:
Postman
curl
And even from the same C# backend, it starts working if I change the request content creation logic to this:
private HttpContent CreateHttpContent(object content)
{
if (content == null)
{
return null;
}
var json = JsonConvert.SerializeObject(content);
return new StringContent(
json,
Encoding.UTF8,
MediaTypeNames.Application.Json);
}After this change, the global endpoint also returns 200 OK.
So now I am confused about what exactly is happening here:
Is Xray validating something specific in the request body formatting?
Is there some difference between StreamContent and StringContent that affects authentication?
Could this be related to content length, encoding, or serialization behavior?
Has anyone faced something similar with the Xray API?
Hi Jubin! Great investigation — the root cause is almost certainly related to how StreamContent serializes and sends the request body compared to StringContent.
Here's what's happening:
1. **StreamContent vs StringContent**: When you use StreamContent with a MemoryStream, the Content-Length header may not be set correctly (or at all), because the length of a stream isn't always known upfront. Some API endpoints and load balancers are strict about requiring a Content-Length header for POST/PUT requests, and this can trigger a 401 or other auth-related errors on specific regional endpoints.
2. **Regional endpoint difference**: The EU endpoint (eu.xray.cloud.getxray.app) may be more lenient in accepting chunked/streaming bodies, while the global endpoint enforces stricter HTTP content requirements.
3. **Why StringContent works**: StringContent sets the Content-Length header automatically and also correctly sets Content-Type to application/json, which is likely required by the Xray authentication endpoint.
The fix is to use StringContent (as you discovered), which is actually the recommended approach for JSON payloads in .NET HttpClient. Your working code is the right pattern:
var json = JsonConvert.SerializeObject(content);
return new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json);
This ensures the Content-Type and Content-Length are always set correctly.
Hope this clarifies what was happening! Let me know if you need further help.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.