#7997·spinnaker

Jenkins buildWithParameters returns HTTP 303 for non-concurrent jobs — Igor throws BuildJobError instead of handling redirect

Author: jialaowaiCreated Sep 11, 2026Updated Sep 11, 2026
Labelsbugcomponent/igorci/jenkins

Summary

When a Jenkins job has concurrentBuild=false and is already queued/running with compatible parameters, Jenkins returns HTTP 303 See Other (redirecting to the existing queue item). Igor does not handle this response — OkHttp3 auto-follows the redirect to a 200 OK, and Igor throws BuildJobError("Received a non-201 status when submitting job...").

This causes the entire Spinnaker pipeline stage to fail with buildNumber=null, cascading into failures in waitForJenkinsJobStart, getBuildProperties, and getBuildArtifacts.

Affected Code

Two code paths contain the same bug:

  1. JenkinsService.java line ~190 (triggerBuildWithParameters):
java
public long triggerBuildWithParameters(String job, Map<String, String> queryParameters) {
    Response<ResponseBody> response = buildWithParameters(job, queryParameters);
    if (response.code() != 201) {
      throw new BuildJobError("Received a non-201 status when submitting job '" + job + "'");
    }
    // ...
}
  1. BuildController.groovy line ~287 (the Orca-facing PUT endpoint):
groovy
if (response.code() != 201) {
    throw new BuildJobError("Received a non-201 status when submitting job '${job}' to master '${master}'")
}

Both paths strictly check response.code() != 201 and throw on anything else.

Root problem: OkHttp3's default behavior is followRedirects=true. When Jenkins returns 303, OkHttp3 automatically follows the redirect (GET /queue/item/{id}/), receives a 200 OK, and returns that 200 as the Response to Igor. Igor then sees response.code() == 200 (not 201) and throws the error.

Since BuildController and JenkinsService call jenkinsClient.buildWithParameters() via Retrofit2 → OkHttp3, they never see the original 303 — they only see the post-redirect 200. There is no code anywhere in Igor that inspects redirect history or the Location header from a 303.

When Does Jenkins Return 303?

This is documented Jenkins behavior. When a POST /job/{name}/buildWithParameters request is received for a non-concurrent job (concurrentBuild=false) that is already queued or running with compatible parameters, Jenkins returns:

HTTP/1.1 303 See Other
Location: /queue/item/{existingItemId}/

This tells the caller: "Your build is already queued/running — here's the existing queue item."

This is a valid, non-error response. The build will execute; it's just already in progress.

Steps to Reproduce

  1. Configure a Jenkins master in Spinnaker (Igor)
  2. Create a Jenkins job with concurrentBuild=false (i.e., "Do not allow concurrent builds" checked)
  3. Trigger the job from a Spinnaker pipeline (Pipeline A)
  4. While the build from step 3 is still running/queued, trigger the same job with the same parameters from another Spinnaker pipeline (Pipeline B)
  5. Pipeline B fails with:
    Status: 400, Message: Received a non-201 status when submitting job 'JOB_NAME' to master 'MASTER_NAME'
  6. Downstream tasks fail:
    waitForJenkinsJobStart: Path parameter "item" value must not be null
    getBuildProperties: Can't retrieve property file because the build number is not available
    getBuildArtifacts: Path parameter "buildNumber" value must not be null

This is more likely to occur with multiple Igor replicas (our environment has 8), as different pods may handle the two pipeline triggers independently.

Evidence from Production

From the Jenkins localhost_access.log on Aug 19, 2026 — out of 2,549 buildWithParameters requests from Spinnaker-igor that day, exactly one returned 303:

Time (CEST) Igor Pod IP HTTP Status Job Params
13:44:14 10.41.91.86 201 OSS-CI-Fetch-Build-Upload-EVNFM CHART_NAME=eric-am-onboarding-service, CHART_VERSION=1.530.0-18
13:50:15 10.41.91.67 303 OSS-CI-Fetch-Build-Upload-EVNFM Identical params

The second request came from a different Igor pod 6 minutes later with the same parameters. The build from the first request was still running. Jenkins correctly returned 303 → Igor failed.

Expected Behavior

When Igor receives an HTTP 303 (or a post-redirect 200 from /queue/item/{id}/) from buildWithParameters, it should:

  1. Recognize that the build is already queued/running
  2. Extract the queue item ID from the redirect URL (the Location header or the final URL after auto-redirect)
  3. Return that queue item ID to Orca so it can monitor the existing build
  4. Not treat this as a fatal error

Proposed Fix

Option A (minimal — handle post-redirect 200):

In BuildController.groovy and JenkinsService.triggerBuildWithParameters(), change the status check to also accept 200 when the response URL matches /queue/item/{id}/:

java
// JenkinsService.java
public long triggerBuildWithParameters(String job, Map<String, String> queryParameters) {
    Response<ResponseBody> response = buildWithParameters(job, queryParameters);
    if (response.code() == 201) {
        // Normal path: new build queued
        String queuedLocation = response.headers().values("location").stream()
            .findFirst()
            .orElseThrow(() -> new QueuedJobDeterminationError("Could not find Location header"));
        int lastSlash = queuedLocation.lastIndexOf('/');
        return Long.parseLong(queuedLocation.substring(lastSlash + 1));
    } else if (response.code() == 200) {
        // Possible 303 redirect followed by OkHttp: build already queued
        String requestUrl = response.raw().request().url().toString();
        java.util.regex.Matcher m = java.util.regex.Pattern.compile("/queue/item/(\\d+)").matcher(requestUrl);
        if (m.find()) {
            log.info("Build for job '{}' already queued (queue item {}), monitoring existing build", job, m.group(1));
            return Long.parseLong(m.group(1));
        }
        throw new BuildJobError("Received unexpected 200 status when submitting job '" + job + "'");
    } else {
        throw new BuildJobError("Received a non-201 status when submitting job '" + job + "'");
    }
}

Option B (robust — disable auto-redirect for build POSTs):

Configure the OkHttpClient used by the Jenkins Retrofit client to not follow redirects, then handle 303 explicitly:

In JenkinsConfig.groovy, add .followRedirects(false) to the OkHttpClient builder:

groovy
OkHttpClient.Builder clientBuilder = okHttpClientConfig.createForRetrofit2()
    .readTimeout(timeout, TimeUnit.MILLISECONDS)
    .followRedirects(false)  // Handle 303 explicitly

Then in JenkinsService.java:

java
if (response.code() == 303) {
    String location = response.headers().get("Location");
    // Extract queue item ID from Location header
    // ...
}

Option A is safer as a first fix since it doesn't change the redirect behavior for all Jenkins API calls.

Environment

  • Spinnaker version: 2025.3.2
  • Igor replicas: 8
  • Jenkins job config: concurrentBuild=false
  • OkHttp3 default: followRedirects=true

Workarounds

Until this is fixed upstream:

  1. Add retry to the Spinnaker pipeline Jenkins stage (1–2 retries, 5-min delay). The retry succeeds because the original build completes.
  2. Prevent duplicate triggers by adding a Pipeline Lock stage with a shared key, so only one pipeline triggers the Jenkins job at a time.
  3. Enabling concurrentBuild=true on the Jenkins job is not a viable workaround if the job logic doesn't support it.