Skip to content

Commit 9ba78b0

Browse files
Handle errors properly
1 parent daa2931 commit 9ba78b0

File tree

6 files changed

+88
-62
lines changed

6 files changed

+88
-62
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Functional HTTP server
22

3-
Common Functional Programming Algebraic data types for JavaScript that is compatible with most modern browsers and Deno.
3+
A simple HTTP server inspired by Express and in tune with Functional Programming principles in JavaScript for Deno.
44

55
[![deno land](http://img.shields.io/badge/available%20on-deno.land/x-lightgrey.svg?logo=deno&labelColor=black)](https://github.com/sebastienfilion/[email protected])
66
[![deno version](https://img.shields.io/badge/deno-^1.4.6-lightgrey?logo=deno)](https://github.com/denoland/deno)
@@ -75,7 +75,7 @@ startHTTPServer(
7575
{ port: 8080 },
7676
route(
7777
handlers.get('/', _ => Task.of(Response.OK({ 'content-type': 'text/plain' }, encodeText("Hello, Hoge!"))))
78-
);
78+
)
7979
);
8080
```
8181

@@ -93,7 +93,7 @@ import { explodeRequest } from "https://deno.land/x/[email protected].
9393
startHTTPServer(
9494
{ port: 8080 },
9595
route(
96-
handlers.get('/users', explodeRequest(({ filters }) => retrieveUsers(filters))),
96+
handlers.get('/users', explodeRequest(({ status }) => retrieveUsers({ filters: { status } }))),
9797
handlers.post(/\/users\/(?<userID>.+)$/, explodeRequest(({ userID }, { data: user }) => updateUser(userID, user)))
9898
)
9999
);
@@ -102,7 +102,7 @@ startHTTPServer(
102102
For this sample, a `GET` request made with a query string will be parsed as an object.
103103

104104
```bash
105-
$ curl localhost:8080/users?filters[status]=active
105+
$ curl localhost:8080/users?status=active
106106
```
107107

108108
And, a `POST` request with a body as JSON will be parsed as well.

library/route.js

+7-9
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ import { assertIsRegex } from "https://deno.land/x/[email protected]/library/uti
7676
* startHTTPServer(
7777
* { port: 8080 },
7878
* route(
79-
* handlers.get('/users', explodeRequest(({ filters }) => retrieveUsers(filters))),
79+
* handlers.get('/users', explodeRequest(({ status }) => retrieveUsers({ filters: { status } }))),
8080
* handlers.post(/\/users\/(?<userID>.+)$/, explodeRequest(({ userID }, { data: user }) => updateUser(userID, user)))
8181
* )
8282
* );
@@ -85,7 +85,7 @@ import { assertIsRegex } from "https://deno.land/x/[email protected]/library/uti
8585
* For this sample, a `GET` request made with a query string will be parsed as an object.
8686
*
8787
* ```bash
88-
* $ curl localhost:8080/users?filters[status]=active
88+
* $ curl localhost:8080/users?status=active
8989
* ```
9090
*
9191
* And, a `POST` request with a body as JSON will be parsed as well.
@@ -108,15 +108,13 @@ import { assertIsRegex } from "https://deno.land/x/[email protected]/library/uti
108108
* startHTTPServer({ port: 8080 }, route(...userRoutes, ...sensorRoutes));
109109
* ```
110110
*
111-
* ### middleware
111+
* ### Middleware
112112
*
113-
* This library doesn't come with a special function to create middleware or anything; but that doesn't mean you can't
114-
* do it with some function composition.
113+
* Before talking about middlewares, I think it is important to talk about the power of function composition and couple of
114+
* things special about `startHTTPServer` and `route`:
115115
*
116-
* Before jumping into an example, you need to keep in mind two concepts:
117-
*
118-
* 1. The function `startHTTPServer` takes a unary function that must return a `Task` of `Response`.
119-
* 2. The function `route`, will always return early if the argument is not a `Request`.
116+
* 1. The function `startHTTPServer` takes a unary function that must return a `Task` of `Response`.
117+
* 2. The function `route`, will always return early if the argument is not a `Request`.
120118
*
121119
* So for example, if you needed to discard any request with a content type that is not `application/json`, you could
122120
* do the following.

library/server.js

+26-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { gray, red } from "https://deno.land/[email protected]/fmt/colors.ts";
12
import { serve, serveTLS } from "https://deno.land/[email protected]/http/server.ts";
2-
import { curry, reduce } from "https://x.nest.land/[email protected]/source/index.js";
3+
import { cond, curry, reduce } from "https://x.nest.land/[email protected]/source/index.js";
4+
import { encodeText } from "https://deno.land/x/[email protected]/library/utilities.js";
35
import Request from "https://deno.land/x/[email protected]/library/Request.js";
6+
import Response from "https://deno.land/x/[email protected]/library/Response.js";
47

58
/**
69
* ## Simple HTTP server
@@ -41,10 +44,27 @@ export const stream = curry(
4144
for await (const _request of iterator) {
4245
const { body = new Uint8Array([]), headers, method, url } = _request;
4346

44-
unaryFunction(Request({ ...destructureHeaders(headers), method, url }, await Deno.readAll(body)))
45-
.map(response => _request.respond({ ...response.headers, body: response.raw }))
46-
.run()
47-
.catch(error => console.error(error));
47+
const handleResponse = response => _request.respond({ ...response.headers, body: response.raw });
48+
const handleError = error =>
49+
console.error(red(`An error occurred in an handler: ${error.message}\n${gray(error.stack)}`))
50+
|| _request.respond({ status: 500, body: encodeText(error.message) })
51+
52+
try {
53+
unaryFunction(Request({ ...destructureHeaders(headers), method, url }, await Deno.readAll(body)))
54+
.run()
55+
.then(
56+
container =>
57+
container.fold({
58+
Left: cond([
59+
[ Response.is, handleResponse ],
60+
[ _ => true, handleError ]
61+
]),
62+
Right: handleResponse
63+
}),
64+
);
65+
} catch (error) {
66+
handleError(error);
67+
}
4868
}
4969
}
5070
);
@@ -60,4 +80,4 @@ export const startHTTPServer = (options, unaryFunction) => {
6080
return server;
6181
};
6282

63-
export default startHTTPServer;
83+
export default startHTTPServer;

library/server_test.js

+42-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { assert, assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
2+
import { compose, converge, mergeRight } from "https://x.nest.land/[email protected]/source/index.js";
23

4+
import Either from "https://deno.land/x/[email protected]/library/Either.js";
35
import Task from "https://deno.land/x/[email protected]/library/Task.js";
46
import { decodeRaw, encodeText, safeExtract } from "https://deno.land/x/[email protected]/library/utilities.js";
57
import { fetch } from "https://deno.land/x/[email protected]/library/browser_safe.js";
@@ -8,17 +10,21 @@ import Response from "https://deno.land/x/[email protected]/library/Response.
810

911
import { handlers, route } from "./route.js";
1012
import { startHTTPServer } from "./server.js";
11-
import { authorizeRequest, explodeRequest } from "./utilities.js";
13+
import { factorizeMiddleware, explodeRequest } from "./utilities.js";
1214

13-
const authorize = authorizeRequest(_ => Task.of({ authorizationToken: "hoge" }));
15+
const authorize = factorizeMiddleware(request =>
16+
request.headers["accept"] === 'application/json'
17+
? Task.of({ authorizationToken: "hoge" })
18+
: Task(_ => Either.Left(Response.BadRequest({}, new Uint8Array([]))))
19+
);
1420

1521
const routeHandlers = [
1622
handlers.get('/', _ => Task.of(Response.OK({}, new Uint8Array([])))),
1723
handlers.post('/hoge', request => Task.of(Response.OK({}, request.raw))),
1824
handlers.get(
1925
'/hoge',
2026
explodeRequest(
21-
({ 'filters[status]': status }) =>
27+
({ status }) =>
2228
Task.of(Response.OK({}, encodeText(JSON.stringify({ status }))))
2329
)
2430
),
@@ -28,17 +34,13 @@ const routeHandlers = [
2834
),
2935
handlers.put(
3036
'/hoge',
31-
explodeRequest(
32-
(meta, body) =>
33-
Task.of(Response.OK({}, encodeText(JSON.stringify(body))))
34-
)
37+
explodeRequest((meta, body) => Task.of(Response.OK({}, encodeText(JSON.stringify(body)))))
3538
),
3639
handlers.post(
3740
'/fuga/piyo',
3841
authorize(
39-
// explodeRequest(
40-
({ authorizationToken }) => Task.of(Response.OK({}, encodeText(JSON.stringify({ authorizationToken }))))
41-
// )
42+
({ authorizationToken }) =>
43+
Task.of(Response.OK({}, encodeText(JSON.stringify({ authorizationToken }))))
4244
)
4345
)
4446
];
@@ -79,11 +81,11 @@ Deno.test(
7981
);
8082

8183
Deno.test(
82-
"startHTTPServer with explodeRequest: GET /hoge",
84+
"startHTTPServer with explodeRequest: GET /hoge?status=active",
8385
async () => {
8486
const server = startHTTPServer({ port: 8080 }, route(...routeHandlers));
8587

86-
const container = await fetch(Request.GET('http://localhost:8080/hoge?filters[status]=active')).run()
88+
const container = await fetch(Request.GET('http://localhost:8080/hoge?status=active')).run()
8789

8890
const response = safeExtract("Failed to unpack the response", container);
8991

@@ -169,3 +171,31 @@ Deno.test(
169171
server.close();
170172
}
171173
);
174+
175+
Deno.test(
176+
"startHTTPServer with explodeRequest: POST /fuga/piyo -- unauthorized",
177+
async () => {
178+
const server = startHTTPServer({ port: 8080 }, route(...routeHandlers));
179+
180+
const container = await fetch(
181+
Request(
182+
{
183+
headers: {
184+
'accept': 'text/plain',
185+
'content-type': 'application/json'
186+
},
187+
method: 'POST',
188+
url: 'http://localhost:8080/fuga/piyo'
189+
},
190+
new Uint8Array([])
191+
)
192+
).run()
193+
194+
const response = safeExtract("Failed to unpack the response", container);
195+
196+
assert(Response.is(response));
197+
assertEquals(response.headers.status, 400);
198+
199+
server.close();
200+
}
201+
);

library/utilities.js

-8
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,3 @@ export const explodeRequest = parseRequest([ parseMeta, parseBody ]);
9595
// factorizeMiddleware :: (Request -> Task a) -> (Request -> a -> Task Response) -> Request -> Task Response
9696
export const factorizeMiddleware = middlewareFunction => handlerFunction =>
9797
request => middlewareFunction(request).chain(options => handlerFunction(options, request));
98-
99-
// authorize :: (Request -> Task a) -> (a -> Task Response) -> Task Response
100-
export const authorizeRequest = authorizationFunction => factorizeMiddleware(
101-
compose(
102-
// Note: Need to implement `alt` for Task :thinking:
103-
authorizationFunction
104-
)
105-
);

library/utilities_test.js

+9-23
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,18 @@
11
import { assert, assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"
22
import { curry } from "https://x.nest.land/[email protected]/source/index.js";
33

4-
import Task from "https://deno.land/x/[email protected]/library/Task.js";
5-
import { decodeRaw, encodeText, safeExtract } from "https://deno.land/x/[email protected]/library/utilities.js";
64
import Request from "https://deno.land/x/[email protected]/library/Request.js";
75
import Response from "https://deno.land/x/[email protected]/library/Response.js";
6+
import Task from "https://deno.land/x/[email protected]/library/Task.js";
7+
import { decodeRaw, encodeText, safeExtract } from "https://deno.land/x/[email protected]/library/utilities.js";
88

9-
import { authorizeRequest, explodeRequest, factorizeMiddleware, parseBody, parseQueryString, parseURLParameters } from "./utilities.js";
10-
11-
Deno.test(
12-
"authorizeRequest",
13-
async () => {
14-
const middleware = authorizeRequest(_ => Task.of({ hoge: "hoge" }));
15-
const handler = middleware(
16-
curry((options, _) => Task.of(Response.OK({}, encodeText(JSON.stringify(options)))))
17-
);
18-
const request = Request({}, new Uint8Array([]));
19-
20-
const container = await handler(request).run()
21-
22-
const response = safeExtract("Failed to authorize the request.", container);
23-
24-
assertEquals(
25-
JSON.parse(decodeRaw(response.raw)),
26-
{ hoge: "hoge" }
27-
);
28-
}
29-
);
9+
import {
10+
explodeRequest,
11+
factorizeMiddleware,
12+
parseBody,
13+
parseQueryString,
14+
parseURLParameters
15+
} from "./utilities.js";
3016

3117
Deno.test(
3218
"explodeRequest",

0 commit comments

Comments
 (0)