Skip to content

Commit 709ad40

Browse files
authored
feat(dart_frog): cache Request and Response body (#470)
1 parent f2c9a5e commit 709ad40

File tree

11 files changed

+331
-6
lines changed

11 files changed

+331
-6
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'dart:io';
2+
3+
import 'package:http/http.dart' as http;
4+
import 'package:test/test.dart';
5+
6+
void main() {
7+
group('E2E (/messages)', () {
8+
test('GET /messages responds with 405', () async {
9+
final response = await http.get(
10+
Uri.parse('http://localhost:8080/messages'),
11+
);
12+
expect(response.statusCode, equals(HttpStatus.methodNotAllowed));
13+
});
14+
15+
test('POST /messages responds with 400 when body is empty', () async {
16+
final response = await http.post(
17+
Uri.parse('http://localhost:8080/messages'),
18+
);
19+
expect(response.statusCode, equals(HttpStatus.badRequest));
20+
});
21+
22+
test(
23+
'POST /messages responds with 200 when body is not empty',
24+
() async {
25+
const message = 'hello world';
26+
final response = await http.post(
27+
Uri.parse('http://localhost:8080/messages'),
28+
body: message,
29+
);
30+
expect(response.statusCode, equals(HttpStatus.ok));
31+
expect(response.body, equals('message: $message'));
32+
},
33+
skip: true,
34+
);
35+
});
36+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'dart:io';
2+
3+
import 'package:dart_frog/dart_frog.dart';
4+
5+
Handler middleware(Handler handler) {
6+
return handler.use(_requestValidator());
7+
}
8+
9+
Middleware _requestValidator() {
10+
return (handler) {
11+
return (context) async {
12+
final request = context.request;
13+
14+
if (request.method != HttpMethod.post) {
15+
return Response(statusCode: HttpStatus.methodNotAllowed);
16+
}
17+
18+
final body = await request.body();
19+
20+
if (body.isEmpty) return Response(statusCode: HttpStatus.badRequest);
21+
22+
return handler(context);
23+
};
24+
};
25+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import 'package:dart_frog/dart_frog.dart';
2+
3+
Future<Response> onRequest(RequestContext context) async {
4+
final body = await context.request.body();
5+
return Response(body: 'message: $body');
6+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'dart:io';
2+
3+
import 'package:dart_frog/dart_frog.dart';
4+
import 'package:mocktail/mocktail.dart';
5+
import 'package:test/test.dart';
6+
7+
import '../../../routes/messages/_middleware.dart';
8+
9+
class _MockRequestContext extends Mock implements RequestContext {}
10+
11+
void main() {
12+
group('middleware', () {
13+
final handler = middleware(
14+
(context) async {
15+
final body = await context.request.body();
16+
return Response(body: 'body: $body');
17+
},
18+
);
19+
20+
test('returns 405 when method is not POST', () async {
21+
final request = Request.get(Uri.parse('http://localhost/'));
22+
final context = _MockRequestContext();
23+
when(() => context.request).thenReturn(request);
24+
25+
final response = await handler(context);
26+
expect(response.statusCode, equals(HttpStatus.methodNotAllowed));
27+
});
28+
29+
test('returns 400 when POST body is empty', () async {
30+
final request = Request.post(Uri.parse('http://localhost/'));
31+
final context = _MockRequestContext();
32+
when(() => context.request).thenReturn(request);
33+
34+
final response = await handler(context);
35+
expect(response.statusCode, equals(HttpStatus.badRequest));
36+
});
37+
38+
test('returns 200 when POST body is not empty', () async {
39+
const body = '__test_body__';
40+
final request = Request.post(Uri.parse('http://localhost/'), body: body);
41+
final context = _MockRequestContext();
42+
when(() => context.request).thenReturn(request);
43+
44+
final response = await handler(context);
45+
expect(response.statusCode, equals(HttpStatus.ok));
46+
expect(response.body(), completion(equals('body: $body')));
47+
});
48+
});
49+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'dart:io';
2+
3+
import 'package:dart_frog/dart_frog.dart';
4+
import 'package:mocktail/mocktail.dart';
5+
import 'package:test/test.dart';
6+
7+
import '../../../routes/messages/index.dart' as route;
8+
9+
class _MockRequestContext extends Mock implements RequestContext {}
10+
11+
void main() {
12+
group('POST /', () {
13+
test('responds with a 200 and message in body', () async {
14+
const message = 'Hello World';
15+
final request = Request.post(
16+
Uri.parse('http://localhost/'),
17+
body: message,
18+
);
19+
final context = _MockRequestContext();
20+
when(() => context.request).thenReturn(request);
21+
final response = await route.onRequest(context);
22+
expect(response.statusCode, equals(HttpStatus.ok));
23+
expect(response.body(), completion(equals('message: $message')));
24+
});
25+
});
26+
}

packages/dart_frog/lib/src/_internal.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:collection';
23
import 'dart:convert';
34
import 'dart:io';

packages/dart_frog/lib/src/request.dart

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class Request {
9393

9494
Request._(this._request);
9595

96-
final shelf.Request _request;
96+
shelf.Request _request;
9797

9898
/// Connection information for the associated HTTP request.
9999
HttpConnectionInfo get connectionInfo {
@@ -119,7 +119,24 @@ class Request {
119119
Stream<List<int>> bytes() => _request.read();
120120

121121
/// Returns a [Future] containing the body as a [String].
122-
Future<String> body() => _request.readAsString();
122+
Future<String> body() async {
123+
const requestBodyKey = 'dart_frog.request.body';
124+
final bodyFromContext =
125+
_request.context[requestBodyKey] as Completer<String>?;
126+
if (bodyFromContext != null) return bodyFromContext.future;
127+
128+
final completer = Completer<String>();
129+
try {
130+
_request = _request.change(
131+
context: {..._request.context, requestBodyKey: completer},
132+
);
133+
completer.complete(await _request.readAsString());
134+
} catch (error, stackTrace) {
135+
completer.completeError(error, stackTrace);
136+
}
137+
138+
return completer.future;
139+
}
123140

124141
/// Returns a [Future] containing the form data as a [Map].
125142
Future<Map<String, String>> formData() {
@@ -129,7 +146,7 @@ class Request {
129146
/// Returns a [Future] containing the body text parsed as a json object.
130147
/// This object could be anything that can be represented by json
131148
/// e.g. a map, a list, a string, a number, a bool...
132-
Future<dynamic> json() async => jsonDecode(await _request.readAsString());
149+
Future<dynamic> json() async => jsonDecode(await body());
133150

134151
/// Creates a new [Request] by copying existing values and applying specified
135152
/// changes.

packages/dart_frog/lib/src/response.dart

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class Response {
4848

4949
Response._(this._response);
5050

51-
final shelf.Response _response;
51+
shelf.Response _response;
5252

5353
/// The HTTP status code of the response.
5454
int get statusCode => _response.statusCode;
@@ -61,7 +61,24 @@ class Response {
6161
Stream<List<int>> bytes() => _response.read();
6262

6363
/// Returns a [Future] containing the body as a [String].
64-
Future<String> body() => _response.readAsString();
64+
Future<String> body() async {
65+
const responseBodyKey = 'dart_frog.response.body';
66+
final bodyFromContext =
67+
_response.context[responseBodyKey] as Completer<String>?;
68+
if (bodyFromContext != null) return bodyFromContext.future;
69+
70+
final completer = Completer<String>();
71+
try {
72+
_response = _response.change(
73+
context: {..._response.context, responseBodyKey: completer},
74+
);
75+
completer.complete(await _response.readAsString());
76+
} catch (error, stackTrace) {
77+
completer.completeError(error, stackTrace);
78+
}
79+
80+
return completer.future;
81+
}
6582

6683
/// Returns a [Future] containing the form data as a [Map].
6784
Future<Map<String, String>> formData() {
@@ -71,7 +88,7 @@ class Response {
7188
/// Returns a [Future] containing the body text parsed as a json object.
7289
/// This object could be anything that can be represented by json
7390
/// e.g. a map, a list, a string, a number, a bool...
74-
Future<dynamic> json() async => jsonDecode(await _response.readAsString());
91+
Future<dynamic> json() async => jsonDecode(await body());
7592

7693
/// Creates a new [Response] by copying existing values and applying specified
7794
/// changes.

packages/dart_frog/test/src/middleware_test.dart

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,81 @@ void main() {
4242
equals('$stringValue $intValue'),
4343
);
4444
});
45+
46+
test('middleware can be used to read the request body', () async {
47+
Middleware requestValidator() {
48+
return (handler) {
49+
return (context) async {
50+
final body = await context.request.body();
51+
if (body.isEmpty) return Response(statusCode: HttpStatus.badRequest);
52+
return handler(context);
53+
};
54+
};
55+
}
56+
57+
Future<Response> onRequest(RequestContext context) async {
58+
final body = await context.request.body();
59+
return Response(body: 'body: $body');
60+
}
61+
62+
final handler = const Pipeline()
63+
.addMiddleware(requestValidator())
64+
.addHandler(onRequest);
65+
66+
var request = Request.get(Uri.parse('http://localhost/'));
67+
var context = _MockRequestContext();
68+
when(() => context.request).thenReturn(request);
69+
var response = await handler(context);
70+
71+
expect(response.statusCode, equals(HttpStatus.badRequest));
72+
73+
const body = '__test_body__';
74+
request = Request.get(Uri.parse('http://localhost/'), body: body);
75+
context = _MockRequestContext();
76+
when(() => context.request).thenReturn(request);
77+
response = await handler(context);
78+
79+
expect(response.statusCode, equals(HttpStatus.ok));
80+
expect(await response.body(), equals('body: $body'));
81+
});
82+
83+
test('middleware can be used to read the response body', () async {
84+
const emptyBody = '(empty)';
85+
Middleware responseValidator() {
86+
return (handler) {
87+
return (context) async {
88+
final response = await handler(context);
89+
final body = await response.body();
90+
if (body.isEmpty) return Response(body: emptyBody);
91+
return response;
92+
};
93+
};
94+
}
95+
96+
Future<Response> onRequest(RequestContext context) async {
97+
final body = await context.request.body();
98+
return Response(body: body);
99+
}
100+
101+
final handler = const Pipeline()
102+
.addMiddleware(responseValidator())
103+
.addHandler(onRequest);
104+
105+
var request = Request.get(Uri.parse('http://localhost/'));
106+
var context = _MockRequestContext();
107+
when(() => context.request).thenReturn(request);
108+
var response = await handler(context);
109+
110+
expect(response.statusCode, equals(HttpStatus.ok));
111+
expect(response.body(), completion(equals(emptyBody)));
112+
113+
const body = '__test_body__';
114+
request = Request.get(Uri.parse('http://localhost/'), body: body);
115+
context = _MockRequestContext();
116+
when(() => context.request).thenReturn(request);
117+
response = await handler(context);
118+
119+
expect(response.statusCode, equals(HttpStatus.ok));
120+
expect(response.body(), completion(equals(body)));
121+
});
45122
}

packages/dart_frog/test/src/request_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:convert';
23
import 'dart:io';
34

@@ -42,12 +43,49 @@ void main() {
4243
expect(request.bytes(), emits(utf8.encode(body)));
4344
});
4445

46+
test('throws exception when unable to read body', () async {
47+
final exception = Exception('oops');
48+
final body = Stream<Object>.error(exception);
49+
final request = Request('GET', localhost, body: body);
50+
expect(request.body, throwsA(exception));
51+
});
52+
53+
test('throws exception when unable to read body multiple times', () async {
54+
final exception = Exception('oops');
55+
final body = Stream<Object>.error(exception);
56+
final request = Request('GET', localhost, body: body);
57+
expect(request.body, throwsA(exception));
58+
expect(request.body, throwsA(exception));
59+
});
60+
4561
test('has correct headers', () {
4662
const headers = <String, String>{'foo': 'bar'};
4763
final request = Request('GET', localhost, headers: headers);
4864
expect(request.headers['foo'], equals(headers['foo']));
4965
});
5066

67+
test('body can be read multiple times (sync)', () {
68+
final body = json.encode({'test': 'body'});
69+
final request = Request('GET', localhost, body: body);
70+
71+
expect(request.body(), completion(equals(body)));
72+
expect(request.body(), completion(equals(body)));
73+
74+
expect(request.json(), completion(equals(json.decode(body))));
75+
expect(request.json(), completion(equals(json.decode(body))));
76+
});
77+
78+
test('body can be read multiple times (async)', () async {
79+
final body = json.encode({'test': 'body'});
80+
final request = Request('GET', localhost, body: body);
81+
82+
await expectLater(request.body(), completion(equals(body)));
83+
await expectLater(request.body(), completion(equals(body)));
84+
85+
await expectLater(request.json(), completion(equals(json.decode(body))));
86+
await expectLater(request.json(), completion(equals(json.decode(body))));
87+
});
88+
5189
group('copyWith', () {
5290
test('returns a copy with overridden properties', () {
5391
const headers = <String, String>{'foo': 'bar'};

0 commit comments

Comments
 (0)