Why 301 Redirects Break Salesforce Callouts

Reading Time: 59 min
Author: William Watson Published: January 19, 2026 Updated: January 26, 2026 System: Apex HttpRequest callouts with external APIs Scale: Enterprise integrations with chained callouts Failure: 301/302 responses causing callout failures Audience: Salesforce integration engineers and architects
Jump to section Current: Top of article
Direct answer

Salesforce callouts do not follow redirects automatically, so your integration must either use the final endpoint URL or implement explicit redirect handling with domain allowlists and monitoring.

Introduction

HTTP 301 redirects are a standard mechanism for permanently moving resources to new URLs. Browsers handle them seamlessly, automatically following the redirect chain until reaching the final destination. However, Salesforce’s HTTP callout framework does not follow redirects by default, causing integrations to fail silently or return unexpected responses. This behaviour catches many developers off guard, particularly when external APIs change their URL structures or migrate to new domains.

Understanding why this happens and how to handle it properly is essential for building robust Salesforce integrations.

How HTTP Redirects Work

When a server returns a 301 (Moved Permanently) or 302 (Found/Temporary Redirect) status code, it includes a Location header pointing to the new URL. The expected behaviour is:

  1. Client sends request to original URL
  2. Server responds with 301/302 and Location header
  3. Client automatically sends new request to the Location URL
  4. Process repeats until a non-redirect response is received

Browsers and many HTTP libraries handle this automatically. Salesforce does not.

Why Salesforce Doesn’t Follow Redirects

Salesforce’s HttpRequest class is designed with security and predictability in mind. When you make a callout, Salesforce returns exactly what the server sends back—including redirect responses. This means:

Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://api.example.com/v1/data');
request.setMethod('GET');

HttpResponse response = http.send(request);
System.debug('Status: ' + response.getStatusCode()); // Returns 301, not 200
System.debug('Body: ' + response.getBody()); // Empty or redirect HTML

If api.example.com/v1/data redirects to api.example.com/v2/data, the above code receives the 301 response rather than following it to get the actual data.

The Security Rationale

This behaviour exists for valid security reasons:

  • Prevent open redirects: Automatic redirect following could expose your org to malicious redirect chains
  • Remote Site Settings enforcement: Each URL must be explicitly authorised; automatic redirects could bypass this
  • Credential leakage prevention: Following redirects could send authentication headers to unintended domains
  • Predictable behaviour: Developers maintain explicit control over which endpoints receive requests

Common Scenarios That Trigger Redirect Issues

Domain Migrations

External services frequently migrate domains:

  • api.oldcompany.comapi.newcompany.com
  • http://api.example.comhttps://api.example.com

API Version Changes

APIs often redirect old versions to new ones:

  • /api/v1/resource/api/v2/resource

URL Normalisation

Servers may enforce trailing slashes or specific URL formats:

  • /api/users/api/users/
  • /API/Users/api/users

Load Balancer and CDN Behaviour

Infrastructure changes can introduce redirects:

  • Regional routing redirects
  • Authentication gateway redirects
  • CDN edge node redirects

Decision and Trade-offs

Choose your redirect strategy based on risk profile, not convenience. The table below helps you decide quickly:

Strategy Best for Risks to manage
Update to final endpoint URL Stable APIs where final destination is known Requires config hygiene when providers change domains again
Manual redirect following in Apex APIs with occasional redirect behaviour or dynamic routing Must validate redirect domains and cap redirect depth
Named Credentials + controlled redirect handler Enterprise integrations needing auth and endpoint governance More setup overhead, but strongest operational control
Ignore redirects and retry blindly Never recommended Masks root cause and can amplify failures

Detecting Redirect Issues

Before implementing solutions, confirm that redirects are the problem:

public static void debugCallout(String endpoint) {
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint(endpoint);
    request.setMethod('GET');
    request.setTimeout(30000);

    HttpResponse response = http.send(request);

    System.debug('Status Code: ' + response.getStatusCode());
    System.debug('Status: ' + response.getStatus());

    // Check for redirect status codes
    Integer statusCode = response.getStatusCode();
    if (statusCode == 301 || statusCode == 302 || statusCode == 307 || statusCode == 308) {
        System.debug('REDIRECT DETECTED');
        System.debug('Location Header: ' + response.getHeader('Location'));
    }

    // Log all headers for debugging
    for (String header : response.getHeaderKeys()) {
        System.debug('Header - ' + header + ': ' + response.getHeader(header));
    }
}

Top Tip: Always log the Location header when troubleshooting integration issues. A missing or unexpected redirect is one of the most common causes of “working locally but failing in Salesforce” scenarios.

Solution 1: Update the Endpoint URL

The simplest solution is to use the final destination URL directly. If you discover that your endpoint redirects, update your configuration to point to the final URL.

// Instead of the redirecting URL
// request.setEndpoint('http://api.example.com/data');

// Use the final destination
request.setEndpoint('https://api.example.com/v2/data');

Remember to update your Remote Site Settings or Named Credentials accordingly.

Best Practice: When integrating with external APIs, test the endpoint URL using a tool like Postman or cURL with redirect following disabled (curl -L follows redirects, omit -L to see the raw response). This reveals the actual behaviour Salesforce will encounter.

Solution 2: Implement Manual Redirect Following

When you cannot control the endpoint URL or need to handle dynamic redirects, implement redirect following in your Apex code:

public class HttpCalloutWithRedirect {

    private static final Integer MAX_REDIRECTS = 5;
    private static final Set<Integer> REDIRECT_CODES = new Set<Integer>{301, 302, 307, 308};

    public static HttpResponse sendWithRedirect(HttpRequest originalRequest) {
        Http http = new Http();
        HttpRequest request = originalRequest;
        HttpResponse response;
        Integer redirectCount = 0;

        while (redirectCount < MAX_REDIRECTS) {
            response = http.send(request);

            if (!REDIRECT_CODES.contains(response.getStatusCode())) {
                return response;
            }

            String newLocation = response.getHeader('Location');
            if (String.isBlank(newLocation)) {
                throw new CalloutException('Redirect response missing Location header');
            }

            // Handle relative URLs
            newLocation = resolveRedirectUrl(request.getEndpoint(), newLocation);

            // Create new request for redirect destination
            request = new HttpRequest();
            request.setEndpoint(newLocation);
            request.setMethod(getRedirectMethod(originalRequest.getMethod(), response.getStatusCode()));
            request.setTimeout(originalRequest.getTimeout());

            // Copy headers (excluding Host which should be recalculated)
            copyHeaders(originalRequest, request);

            redirectCount++;
        }

        throw new CalloutException('Maximum redirect limit (' + MAX_REDIRECTS + ') exceeded');
    }

    private static String resolveRedirectUrl(String originalUrl, String location) {
        // Handle absolute URLs
        if (location.startsWith('http://') || location.startsWith('https://')) {
            return location;
        }

        // Handle protocol-relative URLs
        if (location.startsWith('//')) {
            return 'https:' + location;
        }

        // Handle relative URLs
        Url original = new Url(originalUrl);
        if (location.startsWith('/')) {
            return original.getProtocol() + '://' + original.getHost() + location;
        }

        // Handle relative path (same directory)
        String path = original.getPath();
        Integer lastSlash = path.lastIndexOf('/');
        String basePath = lastSlash > 0 ? path.substring(0, lastSlash + 1) : '/';
        return original.getProtocol() + '://' + original.getHost() + basePath + location;
    }

    private static String getRedirectMethod(String originalMethod, Integer statusCode) {
        // 307 and 308 preserve the original method
        if (statusCode == 307 || statusCode == 308) {
            return originalMethod;
        }
        // 301 and 302 traditionally convert to GET (though this is debated)
        return 'GET';
    }

    private static void copyHeaders(HttpRequest source, HttpRequest target) {
        // Note: HttpRequest doesn't expose getHeaderKeys(), so you must track headers separately
        // or use a wrapper class. This is a simplified example.
    }
}

Usage Example

HttpRequest request = new HttpRequest();
request.setEndpoint('https://api.example.com/v1/data');
request.setMethod('GET');
request.setTimeout(30000);
request.setHeader('Authorization', 'Bearer ' + accessToken);

HttpResponse response = HttpCalloutWithRedirect.sendWithRedirect(request);

if (response.getStatusCode() == 200) {
    // Process successful response
    Map<String, Object> data = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
}

Performance Tip: Each redirect adds latency and consumes a callout from your governor limits. Salesforce allows 100 callouts per transaction—a 5-redirect chain uses 5 of those. Where possible, cache the final URL after discovering it.

Solution 3: Use Named Credentials with External Credentials

Named Credentials provide a more robust solution for managing endpoints and authentication. While they don’t automatically follow redirects, they centralise endpoint management:

HttpRequest request = new HttpRequest();
request.setEndpoint('callout:My_Named_Credential/api/data');
request.setMethod('GET');

Http http = new Http();
HttpResponse response = http.send(request);

When the external service URL changes, update the Named Credential configuration rather than modifying code.

Setting Up Named Credentials

  1. Navigate to Setup → Named Credentials
  2. Create a new Named Credential with the correct (non-redirecting) endpoint
  3. Configure authentication as required
  4. Reference it in code using the callout: prefix

Best Practice: Combine Named Credentials with the redirect-following code above. Use Named Credentials for the base URL and authentication, then handle any unexpected redirects programmatically.

Solution 4: External Services and API Specifications

For REST APIs with OpenAPI/Swagger specifications, External Services can simplify integration. However, the same redirect limitations apply—ensure the specification points to the final endpoint URL.

When registering an External Service:

  1. Verify the API specification uses the correct base URL
  2. Test endpoints individually for redirect behaviour
  3. Update the specification if the provider changes URLs

Handling Cross-Domain Redirects Securely

Cross-domain redirects require additional Remote Site Settings. If your integration might redirect to a different domain, you must authorise both:

Original: https://api.example.com → Remote Site Setting required
Redirect: https://cdn.example.com → Additional Remote Site Setting required

Without the second Remote Site Setting, the redirect following code throws a CalloutException.

Security Considerations

When implementing redirect following for cross-domain scenarios:

private static final Set<String> ALLOWED_REDIRECT_DOMAINS = new Set<String>{
    'api.example.com',
    'cdn.example.com',
    'auth.example.com'
};

private static void validateRedirectDomain(String redirectUrl) {
    Url parsed = new Url(redirectUrl);
    String host = parsed.getHost().toLowerCase();

    if (!ALLOWED_REDIRECT_DOMAINS.contains(host)) {
        throw new CalloutException('Redirect to unauthorised domain: ' + host);
    }
}

Best Practice: Maintain an explicit allowlist of permitted redirect domains. Never blindly follow redirects to arbitrary domains, as this creates security vulnerabilities.

Monitoring and Alerting

Implement monitoring to detect when redirects start occurring unexpectedly:

public class IntegrationMonitor {

    public static void logCalloutResult(String integrationName, HttpResponse response) {
        Integer statusCode = response.getStatusCode();

        // Log redirects as warnings for investigation
        if (statusCode >= 300 && statusCode < 400) {
            String location = response.getHeader('Location');

            // Create a custom object record or platform event for monitoring
            Integration_Log__c log = new Integration_Log__c(
                Integration_Name__c = integrationName,
                Status_Code__c = statusCode,
                Redirect_Location__c = location,
                Timestamp__c = DateTime.now(),
                Severity__c = 'Warning'
            );
            insert log;

            // Optionally publish a platform event for real-time alerting
            Integration_Alert__e alert = new Integration_Alert__e(
                Integration_Name__c = integrationName,
                Message__c = 'Unexpected redirect detected to: ' + location
            );
            EventBus.publish(alert);
        }
    }
}

Testing Redirect Handling

Write unit tests that verify redirect behaviour:

@isTest
public class HttpCalloutWithRedirectTest {

    @isTest
    static void testRedirectFollowing() {
        // Set up mock that returns a redirect
        Test.setMock(HttpCalloutMock.class, new RedirectMock());

        HttpRequest request = new HttpRequest();
        request.setEndpoint('https://api.example.com/old-endpoint');
        request.setMethod('GET');
        request.setTimeout(30000);

        Test.startTest();
        HttpResponse response = HttpCalloutWithRedirect.sendWithRedirect(request);
        Test.stopTest();

        System.assertEquals(200, response.getStatusCode(), 'Should follow redirect to successful response');
        System.assertEquals('{"data":"success"}', response.getBody());
    }

    @isTest
    static void testMaxRedirectLimit() {
        Test.setMock(HttpCalloutMock.class, new InfiniteRedirectMock());

        HttpRequest request = new HttpRequest();
        request.setEndpoint('https://api.example.com/infinite-loop');
        request.setMethod('GET');

        Test.startTest();
        try {
            HttpCalloutWithRedirect.sendWithRedirect(request);
            System.assert(false, 'Should have thrown exception for max redirects');
        } catch (CalloutException e) {
            System.assert(e.getMessage().contains('Maximum redirect limit'), 'Should indicate max redirects exceeded');
        }
        Test.stopTest();
    }

    private class RedirectMock implements HttpCalloutMock {
        private Integer callCount = 0;

        public HttpResponse respond(HttpRequest request) {
            HttpResponse response = new HttpResponse();

            if (callCount == 0) {
                response.setStatusCode(301);
                response.setHeader('Location', 'https://api.example.com/new-endpoint');
                callCount++;
            } else {
                response.setStatusCode(200);
                response.setBody('{"data":"success"}');
            }

            return response;
        }
    }

    private class InfiniteRedirectMock implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest request) {
            HttpResponse response = new HttpResponse();
            response.setStatusCode(301);
            response.setHeader('Location', 'https://api.example.com/another-redirect');
            return response;
        }
    }
}

Debugging Checklist

When a previously stable integration starts failing, run this sequence first:

  1. Confirm whether the response status changed from 2xx to 301/302/307/308
  2. Inspect the Location header and compare host/path against expected endpoint contract
  3. Verify Remote Site Settings or Named Credential coverage for the redirect destination
  4. Check whether auth headers are still valid after redirect target change
  5. Confirm redirect depth does not exceed your guardrail (MAX_REDIRECTS)
  6. Review integration logs for sudden spikes in redirect responses or latency

Conclusion

Salesforce’s decision not to automatically follow HTTP redirects is a deliberate security choice, but it creates challenges for integrations. The key points to remember:

  • Salesforce returns redirect responses directly rather than following them automatically
  • Update endpoint URLs to point to final destinations when possible
  • Implement redirect following in Apex when dynamic handling is required
  • Validate redirect domains to maintain security
  • Monitor for unexpected redirects to catch integration issues early
  • Configure Remote Site Settings for all domains in the redirect chain

By understanding this behaviour and implementing appropriate solutions, you build integrations that remain stable even as external services evolve their URL structures.

Need to stabilise a failing Salesforce integration?

I support short specialist contracts focused on integration reliability, failure diagnosis, and production hardening.

Discuss your integration issue