Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement interception for send and MultipartRequests #94

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,13 @@ class LoggerInterceptor implements InterceptorContract {
print(data.toString());
return data;
}

@override
Future<StreamedResponseData> interceptStreamedResponse({required StreamedResponseData data}) async {
print("----- Response -----");
print(data.toString());
return data;
}
}

const String appToken = "TOKEN";
Expand All @@ -342,6 +349,9 @@ class WeatherApiInterceptor implements InterceptorContract {
@override
Future<ResponseData> interceptResponse({required ResponseData data}) async =>
data;

@override
Future<StreamedResponseData> interceptStreamedResponse({required StreamedResponseData data}) async => data;
}

class ExpiredTokenRetryPolicy extends RetryPolicy {
Expand Down
64 changes: 56 additions & 8 deletions lib/http/intercepted_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,13 @@ class InterceptedClient extends BaseClient {
});
}

// TODO(codingalecr): Implement interception from `send` method.
@override
Future<StreamedResponse> send(BaseRequest request) {
return _inner.send(request);
Future<StreamedResponse> send(BaseRequest request) async {
var response = await _attemptStreamedRequest(request);

response = await _interceptStreamedResponse(response);

return response;
}

Future<Response> _sendUnstreamed({
Expand Down Expand Up @@ -238,14 +241,13 @@ class InterceptedClient extends BaseClient {
final interceptedRequest = await _interceptRequest(request);

var stream = requestTimeout == null
? await send(interceptedRequest)
: await send(interceptedRequest).timeout(requestTimeout!);
? await _inner.send(interceptedRequest)
: await _inner.send(interceptedRequest).timeout(requestTimeout!);

response = await Response.fromStream(stream);
if (retryPolicy != null &&
retryPolicy!.maxRetryAttempts > _retryCount &&
await retryPolicy!.shouldAttemptRetryOnResponse(
ResponseData.fromHttpResponse(response))) {
await retryPolicy!.shouldAttemptRetryOnResponse(ResponseData.fromHttpResponse(response))) {
_retryCount += 1;
return _attemptRequest(request);
}
Expand All @@ -264,8 +266,42 @@ class InterceptedClient extends BaseClient {
return response;
}

/// Attempts to perform the request and intercept the data
/// of the response
Future<StreamedResponse> _attemptStreamedRequest(BaseRequest request) async {
StreamedResponse response;
try {
// Intercept request
final interceptedRequest = await _interceptRequest(request);

response = requestTimeout == null
? await _inner.send(interceptedRequest)
: await _inner.send(interceptedRequest).timeout(requestTimeout!);

if (retryPolicy != null &&
retryPolicy!.maxRetryAttempts > _retryCount &&
await retryPolicy!
.shouldAttemptRetryOnResponse(StreamedResponseData.fromHttpResponse(response).toResponseData())) {
_retryCount += 1;
return _attemptStreamedRequest(request);
}
} on Exception catch (error) {
if (retryPolicy != null &&
retryPolicy!.maxRetryAttempts > _retryCount &&
retryPolicy!.shouldAttemptRetryOnException(error)) {
_retryCount += 1;
return _attemptStreamedRequest(request);
} else {
rethrow;
}
}

_retryCount = 0;
return response;
}

/// This internal function intercepts the request.
Future<Request> _interceptRequest(Request request) async {
Future<BaseRequest> _interceptRequest(BaseRequest request) async {
for (InterceptorContract interceptor in interceptors) {
RequestData interceptedData = await interceptor.interceptRequest(
data: RequestData.fromHttpRequest(request),
Expand All @@ -288,6 +324,18 @@ class InterceptedClient extends BaseClient {
return response;
}

/// This internal function intercepts the response.
Future<StreamedResponse> _interceptStreamedResponse(StreamedResponse response) async {
for (InterceptorContract interceptor in interceptors) {
StreamedResponseData responseData = await interceptor.interceptStreamedResponse(
data: StreamedResponseData.fromHttpResponse(response),
);
response = responseData.toHttpResponse();
}

return response;
}

void close() {
_inner.close();
}
Expand Down
5 changes: 5 additions & 0 deletions lib/http/interceptor_contract.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ abstract class InterceptorContract {
Future<RequestData> interceptRequest({required RequestData data});

Future<ResponseData> interceptResponse({required ResponseData data});

Future<StreamedResponseData> interceptStreamedResponse({required StreamedResponseData data}) async {
final response = await interceptResponse(data: data.toResponseData());
return StreamedResponseData.fromResponseData(response, data.stream);
}
}
1 change: 1 addition & 0 deletions lib/models/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export 'request_data.dart';
export 'response_data.dart';
export 'http_interceptor_exception.dart';
export 'retry_policy.dart';
export 'streamed_response_data.dart';
50 changes: 37 additions & 13 deletions lib/models/request_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import 'package:http_interceptor/extensions/extensions.dart';
import 'package:http_interceptor/http/http.dart';
import 'package:http_interceptor/utils/utils.dart';

class MultipartBody {
Map<String, String>? fields;
List<MultipartFile>? files;
MultipartBody({this.fields, this.files});
}

/// A class that mimics HTTP Request in order to intercept it's data.
class RequestData {
/// The HTTP method of the request.
Expand Down Expand Up @@ -45,8 +51,8 @@ class RequestData {

/// Creates a new request data from an HTTP request.
///
/// For now it only supports [Request].
/// TODO(codingalecr): Support for [MultipartRequest] and [StreamedRequest].
/// For now it only supports [Request] and [MultipartRequest].
/// TODO(codingalecr): Support for [StreamedRequest].
factory RequestData.fromHttpRequest(BaseRequest request) {
var params = Map<String, dynamic>();
request.url.queryParametersAll.forEach((key, value) {
Expand All @@ -63,6 +69,14 @@ class RequestData {
encoding: request.encoding,
params: params,
);
} else if (request is MultipartRequest) {
return RequestData(
method: methodFromString(request.method),
baseUrl: baseUrl,
headers: request.headers,
body: MultipartBody(fields: request.fields, files: request.files),
params: params,
);
}

throw UnsupportedError(
Expand All @@ -71,25 +85,35 @@ class RequestData {
}

/// Converts this request data to an HTTP request.
Request toHttpRequest() {
BaseRequest toHttpRequest() {
var reqUrl = buildUrlString(baseUrl, params);

Request request = new Request(methodToString(method), reqUrl.toUri());
// Request request = new Request();

late BaseRequest request;

request.headers.addAll(headers);
if (encoding != null) request.encoding = encoding!;
if (body != null) {
if (body is String) {
request.body = body as String;
} else if (body is List) {
request.bodyBytes = body?.cast<int>();
} else if (body is Map) {
request.bodyFields = body.cast<String, String>();
if (body is MultipartBody) {
final _body = body as MultipartBody;
request = MultipartRequest(methodToString(method), reqUrl.toUri())
..files.addAll(_body.files ?? [])
..fields.addAll(_body.fields ?? {});
} else {
throw new ArgumentError('Invalid request body "$body".');
if (body is String) {
request = Request(methodToString(method), reqUrl.toUri())..body = body as String;
} else if (body is List) {
request = Request(methodToString(method), reqUrl.toUri())..bodyBytes = body?.cast<int>();
} else if (body is Map) {
request = Request(methodToString(method), reqUrl.toUri())..bodyFields = body.cast<String, String>();
} else {
throw new ArgumentError('Invalid request body "$body".');
}
if (encoding != null) (request as Request).encoding = encoding!;
}
}

request.headers.addAll(headers);

return request;
}

Expand Down
104 changes: 104 additions & 0 deletions lib/models/streamed_response_data.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'dart:typed_data';

import 'package:http/http.dart';
import 'package:http_interceptor/http/http.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:http_interceptor/models/request_data.dart';

/// A class that mimics a streamed HTTP Response in order to intercept it's data.
class StreamedResponseData {
/// The stream comprising the body of this response.
Stream<List<int>> stream;

/// The HTTP status code for this response.
int statusCode;

Map<String, String>? headers;

/// The size of the response body, in bytes.
///
/// If the size of the request is not known in advance, this is `null`.
int? contentLength;

bool? isRedirect;

/// Whether the server requested that a persistent connection be maintained.
bool? persistentConnection;

/// The (frozen) request that triggered this response.
RequestData? request;

/// Creates a new response data with body bytes.
StreamedResponseData(
this.stream,
this.statusCode, {
this.headers,
this.contentLength,
this.isRedirect,
this.persistentConnection,
this.request,
});

/// Method of the request that triggered this response.
Method? get method => request?.method;

/// URL as String of the request that triggered this response.
String? get url => request?.url;

/// Creates a new response data from an HTTP response.
factory StreamedResponseData.fromHttpResponse(StreamedResponse response) {
return StreamedResponseData(
response.stream,
response.statusCode,
headers: response.headers,
contentLength: response.contentLength,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
request: (response.request != null) ? RequestData.fromHttpRequest(response.request!) : null,
);
}

/// Converts this response data to an HTTP response.
StreamedResponse toHttpResponse() {
return StreamedResponse(
stream,
statusCode,
headers: headers!,
persistentConnection: persistentConnection!,
isRedirect: isRedirect!,
contentLength: contentLength,
request: request?.toHttpRequest(),
);
}

factory StreamedResponseData.fromResponseData(ResponseData response, Stream<List<int>> stream) {
return StreamedResponseData(
stream,
response.statusCode,
headers: response.headers,
persistentConnection: response.persistentConnection,
isRedirect: response.isRedirect,
request: response.request,
contentLength: response.contentLength,
);
}

/// Converts to synchronous response data, with no body
ResponseData toResponseData() {
return ResponseData(
Uint8List(0),
statusCode,
headers: headers,
contentLength: contentLength,
isRedirect: isRedirect,
persistentConnection: persistentConnection,
request: request,
);
}

/// Convenient toString implementation for logging.
@override
String toString() {
return 'StreamedResponseData { $method, $url, $headers, $statusCode, $request }';
}
}
Loading