Dio Redirect Interceptor

中文简体

Background

Dio is a powerful HTTP client library, but its default redirect handling is done by the http library. This means that when a redirect occurs, the http library handles it automatically and returns the final response, and Dio interceptors (such as CookieManager or other custom interceptors) do not get a chance to run. As a result, many features that need to operate during the redirect process (such as saving cookies, modifying request headers, etc.) may not function correctly.

To solve this problem, we provide a custom RedirectInterceptor that can manually handle redirects, ensuring that all custom interceptors (such as CookieManager or others) can function properly during the redirect process.

Features

  • Custom Redirect Handling: By disabling Dio's default redirect behavior, the redirect process is manually handled by the RedirectInterceptor.
  • Works with CookieManager: Ensures that cookies are correctly saved and managed during redirects.
  • Supports Other Custom Interceptors: Not only supports CookieManager, but also works with any other interceptors that need to operate during the redirect process.
  • Redirect Count and URI Tracking: Easily get redirect count, original request URI, and redirect URI through extension methods.

Redirect Behaviour

followRedirects and validateStatus

The interceptor arranges both itself, so a plain Dio() works:

final dio = Dio();
dio.interceptors.add(RedirectInterceptor(() => dio));

It sets followRedirects: false on every request it handles, because a redirect the adapter follows never reaches the interceptor. That alone is not enough: the 3xx response then has to survive validateStatus, which by default rejects anything outside 2xx and would turn every redirect into a badResponse error. So when your validateStatus rejects redirect statuses it is widened to let them through, and your original rule is still applied to any redirect the interceptor does not follow — one past maxRedirectCount, or one your RedirectValidator declined — so those reach you as an error exactly as they would have.

If your validateStatus already accepts 301, 302, 303, 307 and 308, it is left untouched and used as-is.

Credentials and cross-origin redirects

When a redirect crosses to another origin (a different scheme, host or port), authorization, cookie and proxy-authorization are removed from the redirected request, the same rule browsers and curl apply. Pass stripCredentialsOnCrossOrigin: false to forward them anyway:

RedirectInterceptor(() => dio, stripCredentialsOnCrossOrigin: false)

An https request is never redirected to http unless you opt in, and a Location naming any scheme other than http/https is refused:

RedirectInterceptor(() => dio, allowProtocolDowngrade: true)

Interceptor ordering

Each hop is dispatched with Dio.fetch, which re-enters the whole interceptor chain. Two consequences are worth knowing before you place this interceptor:

  • onRequest of every interceptor runs once per hop. An interceptor that writes credentials (options.headers['authorization'] = token) would therefore re-add them on the hop to a foreign origin, undoing the cross-origin stripping. Guard it with options.redirectContext:

    onRequest: (options, handler) {
      final redirect = options.redirectContext;
      if (redirect == null || !redirect.hasLeftOriginalOrigin) {
        options.headers['authorization'] = token;
      }
      handler.next(options);
    }
    

    redirectContext is null on the request the caller made and carries a RedirectContext on every hop after it. It reports facts rather than a verdict, because "is this cross-origin" has more than one useful answer:

    hasLeftOriginalOrigin Has the chain been off originalUri's origin at any point? Set once and never cleared, which makes it the conservative choice for attaching a credential — on a → evil → evil the second hop is same-origin with its predecessor, yet the token still must not go out.
    hopCrossedOrigin Did this hop change origin? The rule browsers apply when dropping credential headers, and what this package uses internally.
    isOnOriginalOrigin Is this request going back to the origin the caller asked for? True for the return leg of an a → identity provider → a login, where a credential for a is usually wanted again.

    It also carries originalUri, previousUri, currentUri and count. options.rawUri and options.redirectCount remain available on every request, redirected or not.

  • Response interceptors registered after this one see the final response exactly once, but never see the 3xx hops: the nested request already carried the response through them, so this interceptor resolves instead of forwarding it a second time. Register an interceptor before this one if it needs to observe every hop.

Method and body

Status Method Body
301, 302 POST becomes GET, others unchanged dropped when the method changes
303 everything but HEAD becomes GET dropped
307, 308 unchanged replayed

A body that is a Stream cannot be replayed; a 307/308 redirect carrying one fails with a DioException wrapping DioRedirectInterceptorException rather than silently sending an empty body.

Errors

Everything raised while following a redirect — a malformed or duplicated Location, a refused downgrade, a non-replayable body — is delivered as a DioException whose error holds the cause, so a single catch covers both transport and redirect failures.

Installation

Add the following dependencies to your pubspec.yaml file:

dependencies:
  dio: ^5.10.0
  dio_redirect_interceptor: ^2.0.0
  dio_cookie_manager: ^3.2.0
  cookie_jar: ^4.0.8

Then run the following command to install the dependencies:

flutter pub get

Setup Example

Create Dio Instance and Use the Interceptors

import 'package:dio/dio.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:dio_redirect_interceptor/dio_redirect_interceptor.dart';

Future<void> main() async {
  // Create a CookieJar instance to store cookies
  final cookieJar = CookieJar();

  // Create a Dio instance and disable the default redirect handling
  final dio = Dio(
    BaseOptions(
      validateStatus: (status) => status != null && status < 400,
      followRedirects: false,  // Disable Dio's default redirect handling
    ),
  );

  // Add interceptors: CookieManager and RedirectInterceptor
  dio.interceptors.addAll([
    CookieManager(cookieJar),  // Manage cookies
    RedirectInterceptor(() => dio),  // Handle redirects
  ]);

  // Perform a request and automatically handle redirects
  final response = await dio.get('http://facebook.com');
  print("statusCode: ${response.statusCode}");
  print("rawRequestOption uri: ${response.rawRequestOption?.uri}");
  print("redirectCount: ${response.redirectCount}");
  print("uri: ${response.requestOptions.uri}");
  print("rawUri: ${response.rawUri}");

  // Get cookies
  final cookies = await cookieJar.loadForRequest(Uri.parse('http://facebook.com'));
  print("Cookies: $cookies");
}

Code Explanation

  • CookieManager: Used to save and load cookies. The CookieJar persists cookies between requests and responses.
  • RedirectInterceptor: A custom interceptor that manually handles HTTP redirects. It disables Dio's default redirect handling, ensuring that CookieManager can properly save cookies.
  • followRedirects: By setting followRedirects to false, we disable Dio's default redirect handling. All redirects are manually handled by the RedirectInterceptor.

Output Example

If you request http://facebook.com, which redirects (e.g., to https://www.facebook.com), the output will be:

statusCode: 200
rawRequestOption uri: http://facebook.com
redirectCount: 2
uri: https://www.facebook.com/
rawUri: http://facebook.com
Cookies: [...]

Using Extension Methods to Get Redirect Information

This library provides extension methods on Response to easily access detailed redirect information:

RedirectInterceptorResponseExtension Extension

import 'package:dio/dio.dart';
import 'package:dio_redirect_interceptor/src/extension.dart';

extension RedirectInterceptorResponseExtension on Response {
  /// Get the original request options
  RequestOptions? get rawRequestOption {
    return requestOptions.extra[RedirectInterceptor.rawRequestOption] as RequestOptions?;
  }

  /// Get the redirect count
  int get redirectCount {
    return requestOptions.extra[RedirectInterceptor.redirectCount] as int? ?? 0;
  }

  /// Get the original URI
  Uri? get rawUri {
    return requestOptions.extra[RedirectInterceptor.rawUri] as Uri?;
  }
}

Example Code: Get Redirect Information

import 'package:dio/dio.dart';
import 'package:dio_redirect_interceptor/src/extension.dart';
import 'package:dio_redirect_interceptor/src/redirect_interceptor.dart';

Future<void> main() async {
  final dio = Dio(
    BaseOptions(
      validateStatus: (status) => status != null && status < 400,
      followRedirects: false,  // Disable Dio's default redirect handling
    ),
  );
  dio.interceptors.addAll([
    RedirectInterceptor(() => dio),  // Use the RedirectInterceptor
  ]);

  // Perform a request and automatically handle redirects
  final response = await dio.get('http://facebook.com');
  print("statusCode: ${response.statusCode}");
  print("rawRequestOption uri: ${response.rawRequestOption?.uri}");
  print("redirectCount: ${response.redirectCount}");
  print("uri: ${response.requestOptions.uri}");
  print("rawUri: ${response.rawUri}");

  print("-" * 50);

  // Disable redirect
  final response1 = await dio.get(
    'http://facebook.com',
    options: Options(extra: {RedirectInterceptor.followRedirects: false}),
  );
  print("statusCode: ${response1.statusCode}");
  print("rawRequestOption uri: ${response1.rawRequestOption?.uri}");
  print("redirectCount: ${response1.redirectCount}");
  print("uri: ${response1.requestOptions.uri}");
  print("rawUri: ${response1.rawUri}");
}

Example Output

statusCode: 200
rawRequestOption uri: http://facebook.com
redirectCount: 2
uri: https://www.facebook.com/
rawUri: http://facebook.com
--------------------------------------------------
statusCode: 301
rawRequestOption uri: null
redirectCount: 0
uri: http://facebook.com
rawUri: null

Solved Issues

Dio's Default Redirect Handling

Dio's default redirect behavior is handled by the http library, meaning that when a redirect occurs, the http library automatically handles the request and returns the final response. Dio's interceptors do not get a chance to process the redirected request. This behavior causes issues with interceptors that need to perform actions during the redirect, such as CookieManager or any custom interceptors that handle headers, cookies, or other data.

Solution: Disable Default Redirect Handling

By setting the followRedirects option to false, we disable Dio's default redirect behavior. Then, the RedirectInterceptor manually handles redirects, ensuring that interceptors such as CookieManager can function properly during the redirect process. This allows you to control the redirect process more finely and ensures that custom interceptors can work as expected.

Conclusion

The RedirectInterceptor provided by this library effectively solves the issue of Dio's default redirect handling. By disabling Dio's default redirect behavior, you gain full control over the redirect process, ensuring that interceptors like CookieManager and other custom interceptors can function properly during the redirect process. This approach allows for more precise request handling and management, including cookie management, custom header handling, and more.

License

MIT License. See LICENSE for details.