Dart http: "Bad state: Can't finalize a finalized Request" when retrying a http.Request after fetching a new access token
Asked Answered
J

3

12

I'm currently trying to access a Web API in Flutter that requires a JWT access token for authorization. The access token expires after a certain amount of time.

A new access token can be requested with a separate refresh token. Right now this access token refresh is performed as soon as a request returns a 401 response. After that, the failed request should be retried with the new access token.

I'm having trouble with this last step. It seems like a http.BaseRequest can only be sent once. How would I retry the http request with the new token?


As suggested in the dart http readme, I created a subclass of http.BaseClient to add the authorization behavior. Here is a simplified version:

import 'dart:async';

import 'package:http/http.dart' as http;

class AuthorizedClient extends http.BaseClient {
  AuthorizedClient(this._authService) : _inner = http.Client();

  final http.Client _inner;
  final AuthService _authService;

  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    final token = await _authService.getAccessToken();
    request.headers['Authorization'] = 'Bearer $token';

    final response = await _inner.send(request);

    if (response.statusCode == 401) {
      final newToken = await _authService.refreshAccessToken();
      request.headers['Authorization'] = 'Bearer $newToken';

      // throws error: Bad state: Can't finalize a finalized Request
      final retryResponse = await _inner.send(request);

      return retryResponse;
    }

    return response;
  }
}

abstract class AuthService {
  Future<String> getAccessToken();
  Future<String> refreshAccessToken();
}
Judah answered 29/6, 2018 at 7:59 Comment(1)
How to initialize http.Client _inner before sending the api call??The final variable '_inner' can't be read because it's potentially unassigned at this point.Alysa
J
12

Here is what I came up with so far, based on Richard Heap's answer: To resend a request, we have to copy it.

So far I was not able to come up for a solution for stream requests!

http.BaseRequest _copyRequest(http.BaseRequest request) {
  http.BaseRequest requestCopy;

  if(request is http.Request) {
    requestCopy = http.Request(request.method, request.url)
      ..encoding = request.encoding
      ..bodyBytes = request.bodyBytes;
  }
  else if(request is http.MultipartRequest) {
    requestCopy = http.MultipartRequest(request.method, request.url)
      ..fields.addAll(request.fields)
      ..files.addAll(request.files);
  }
  else if(request is http.StreamedRequest) {
    throw Exception('copying streamed requests is not supported');
  }
  else {
    throw Exception('request type is unknown, cannot copy');
  }

  requestCopy
    ..persistentConnection = request.persistentConnection
    ..followRedirects = request.followRedirects
    ..maxRedirects = request.maxRedirects
    ..headers.addAll(request.headers);

  return requestCopy;
}
Judah answered 29/6, 2018 at 16:49 Comment(5)
Sound approach even though it's missing impl for StreamedRequests which might be totally fine depending on the situation. No sense supporting something you don't need.Mammilla
This can work inside a StreamedResponse method. I have the same block as OP, I simply call the _copyRequest method inside the 401 block. Then update the Authorization header with the new token value. This worked well for me. var copyRequest = _copyRequest(request); copyRequest.headers['Authorization'] = 'Bearer <new token value>';Algie
@Judah I am still getting the Bad state: Can't finalize a finalized MultipartFile error after doing like thisAlysa
@VasVasanth I am still getting the Bad state: Can't finalize a finalized MultipartFile error after updating the token on header with the copy request.Alysa
Hi, is there any update on how to make this work with StreamedRequests as well ?Antimicrobial
P
6

You can't send the same BaseRequest twice. Make a new BaseRequest from the first one, and send that copy.

Here's some code (from io_client) to 'clone' a BaseRequest.

  var copyRequest = await _inner.openUrl(request.method, request.url);

  copyRequest
      ..followRedirects = request.followRedirects
      ..maxRedirects = request.maxRedirects
      ..contentLength = request.contentLength == null
          ? -1
          : request.contentLength
      ..persistentConnection = request.persistentConnection;
  request.headers.forEach((name, value) {
    copyRequest.headers.set(name, value);
  });
Preacher answered 29/6, 2018 at 10:43 Comment(1)
Thanks, this lead me into the right direction, though it is not a complete/correct. It does not explain how to copy the different request types, such as MultipartRequest or StreamedRequestJudah
P
1

Seems the problem in sending the same form data without finlize last request, solved the same issue this way :

  static final Dio dio2 = Dio();

  static Future<dynamic> _retry(RequestOptions requestOptions) async {
    final options = new Options(
      method: requestOptions.method,
      headers: (await getHeaders()),
    );

    options.responseType = (ResponseType.json);
    Map<String, String>? data;
    try {
      data = Map.fromEntries(requestOptions.data?.fields);
    } catch (e) {}
    var res = dio2.request<dynamic>(
        requestOptions.baseUrl + requestOptions.path,
        data: data == null ? null : FormData.fromMap(data),
        queryParameters: requestOptions.queryParameters,
        options: options);

    return res;
  }

use it in 401 block this way :

handler.resolve(await _retry(response.requestOptions));
Pennywise answered 11/3, 2023 at 9:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.