Skip to content

Commit d7d2d2c

Browse files
committed
v2.0.0-rc.4 🚀 - Support Raw Body in NextJs handlers
1 parent fe58466 commit d7d2d2c

File tree

5 files changed

+103
-11
lines changed

5 files changed

+103
-11
lines changed

README.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,63 @@ let posts = await Contract.getPosts->Rest.fetch(
396396
)
397397
```
398398

399+
#### Raw Body for Webhooks
400+
401+
To make Raw Body work with Next.js handler, you need to disable the automatic body parsing. One use case for this is to allow you to verify the raw body of a **webhook** request, for example [from Stripe](https://docs.stripe.com/webhooks).
402+
403+
> 🧠 This example uses another great library [ReScript Stripe](https://github.com/enviodev/rescript-stripe)
404+
405+
````rescript
406+
let stripe = Stripe.make("sk_test_...")
407+
408+
type input = {
409+
body: string,
410+
sig: string,
411+
}
412+
let route = Rest.route(() => {
413+
path: "/api/stripe/webhook",
414+
method: Post,
415+
input: s => {
416+
body: s.rawBody(S.string),
417+
sig: s.header("stripe-signature", S.string),
418+
},
419+
responses: [
420+
s => {
421+
s.status(200)
422+
let _ = s.data(S.literal({"received": true}))
423+
Ok()
424+
},
425+
s => {
426+
s.status(400)
427+
Error(s.data(S.string))
428+
},
429+
],
430+
})
431+
432+
// Disable bodyParsing to make Raw Body work
433+
let config: RestNextJs.config = {api: {bodyParser: false}}
434+
435+
let default = RestNextJs.handler(route, async ({input}) => {
436+
stripe
437+
->Stripe.Webhook.constructEvent(
438+
~body=input.body,
439+
~sig=input.sig,
440+
// You can find your endpoint's secret in your webhook settings
441+
~secret="whsec_...",
442+
)
443+
->Result.map(event => {
444+
switch event {
445+
| CustomerSubscriptionCreated({data: {object: subscription}}) =>
446+
await processSubscription(subscription)
447+
| _ => ()
448+
}
449+
})
450+
})
451+
```
452+
399453
#### Current Limitations
400454
401455
- Doesn't support path parameters
402-
- Doesn't support raw body
403456
404457
### [Fastify](https://fastify.dev/)
405458
@@ -520,3 +573,4 @@ let url = Rest.url(
520573
- [x] Server implementation with Fastify
521574
- [ ] NextJs integration
522575
- [ ] Add TS/JS support
576+
````

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rescript-rest",
3-
"version": "2.0.0-rc.3",
3+
"version": "2.0.0-rc.4",
44
"description": "😴 ReScript RPC-like client, contract, and server implementation for a pure REST API",
55
"keywords": [
66
"rest",

src/RestNextJs.res

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
open RescriptSchema
44

5+
module Promise = {
6+
type t<+'a> = promise<'a>
7+
8+
@new
9+
external make: (('a => unit, Js.Exn.t => unit) => unit) => t<'a> = "Promise"
10+
11+
@send
12+
external thenResolve: (t<'a>, 'a => 'b) => t<'b> = "then"
13+
}
14+
515
module Exn = {
616
type error
717

@@ -49,6 +59,13 @@ type rec res = private {
4959
send: Js.Json.t => reply,
5060
}
5161

62+
type apiConfig = {
63+
bodyParser?: bool,
64+
externalResolver?: bool,
65+
responseLimit?: bool,
66+
}
67+
type config = {maxDuration?: int, api?: apiConfig}
68+
5269
type options<'input> = {
5370
input: 'input,
5471
req: req,
@@ -58,7 +75,7 @@ type options<'input> = {
5875
let handler = (route, implementation) => {
5976
let {pathItems, definition, isRawBody, responseSchema, inputSchema} = route->Rest.params
6077

61-
// TOD: Validate that we match the req path
78+
// TODO: Validate that we match the req path
6279
pathItems->Js.Array2.forEach(pathItem => {
6380
switch pathItem {
6481
| Param(param) =>
@@ -68,16 +85,29 @@ let handler = (route, implementation) => {
6885
| Static(_) => ()
6986
}
7087
})
71-
if isRawBody {
72-
panic(
73-
`Route ${definition.path} contains a raw body which is not supported by Next.js handler yet`,
74-
)
75-
}
7688

7789
async (req, res) => {
7890
if req.method !== definition.method {
7991
res.status(404).end()
8092
} else {
93+
if req.body === %raw(`undefined`) {
94+
let rawBody = ref("")
95+
let _ = await Promise.make((resolve, reject) => {
96+
let _ = (req->Obj.magic)["on"]("data", chunk => {
97+
rawBody := rawBody.contents ++ chunk
98+
})
99+
let _ = (req->Obj.magic)["on"]("end", resolve)
100+
let _ = (req->Obj.magic)["on"]("error", reject)
101+
})
102+
(req->Obj.magic)["body"] = isRawBody
103+
? rawBody.contents->Obj.magic
104+
: Js.Json.parseExn(rawBody.contents)
105+
} else if isRawBody {
106+
Js.Exn.raiseError(
107+
"Routes with Raw Body require to disable body parser for your handler. Add `let config: RestNextJs.config = {api: {bodyParser: false}}` to the file with your handler to make it work.",
108+
)
109+
}
110+
81111
switch req->S.parseOrThrow(inputSchema) {
82112
| input =>
83113
try {
@@ -104,7 +134,8 @@ let handler = (route, implementation) => {
104134
`Unexpected error in the ${definition.path} route: ${error->S.Error.message}`,
105135
)
106136
}
107-
| exception S.Raised(error) => res.status(400).send(error->S.Error.message->Js.Json.string)
137+
| exception S.Raised(error) =>
138+
res.status(400).json({"error": error->S.Error.message->Js.Json.string}->Obj.magic)
108139
}
109140
}
110141
}

src/RestNextJs.resi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ type rec res = private {
3131
send: Js.Json.t => reply,
3232
}
3333

34+
type apiConfig = {
35+
bodyParser?: bool,
36+
externalResolver?: bool,
37+
responseLimit?: bool,
38+
}
39+
type config = {maxDuration?: int, api?: apiConfig}
40+
3441
type options<'input> = {
3542
input: 'input,
3643
req: req,

0 commit comments

Comments
 (0)