Repository: octokit/webhooks.js (ts)

Issue:
throw a helpful error if received request has content type set to `application/x-www-form-url-encoded`
I ran into [this issue][1] and though it was never resolved, it appears tha the underlying problem is that `@octokit/wehooks` is [assuming that the incoming request body is `application/json`][2]. However, github can also deliver payloads in `application/x-www-form-url-encoded` which is, in fact, the default delivery format when creating a webhook via the github UI


![image](https://user-images.githubusercontent.com/4205/85878116-d3d63200-b79d-11ea-880a-0f61208543cb.png)

In the event that the payload is encoded as `application/x-www-form-urlencoded`, the middleware will try to parse it as JSON.

I could see two possible solutions:

1. asserting that the `content-type` header is a`application/json`, and rejecting with a 400 and a status text that clearly indicates that `x-form-url-encoded` is not acceptable, and that the webhook configuration should be updated.
2. checking the content-type and decoding the payload appropriately for both `application/json` and `application/x-form-www-url-encoded`.


Would be happy to help implement either.

[1]: https://github.com/octokit/webhooks.js/issues/75
[2]: https://github.com/octokit/webhooks.js/blob/f6552815c887b3651a57e92d9e9b7821dc90ff91/src/middleware/get-payload.js#L18

Pull request description:
fix: return more helpful error when `Content-Type` header is not `application/json`
<!-- Please refer to our contributing docs for any questions on submitting a pull request -->


<!-- Issues are required for both bug fixes and features. -->
Resolves #158

----

## Behavior

### Before the change?
<!-- Please describe the current behavior that you are modifying. -->

* The middleware would fail since it would assume that the `Content-Type` header is set to `application/json`, and it would return an error

### After the change?
<!-- Please describe the behavior or changes that are being added by this PR. -->

* The middleware will return an [HTTP 415](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415) response explaining that only `application/json` is supported 


### Other information
<!-- Any other information that is important to this PR  -->

* 

----

## Additional info

### Pull request checklist
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features)
- [x] Added the appropriate label for the given change

### Does this introduce a breaking change?
<!-- If this introduces a breaking change make sure to note it here any what the impact might be -->

Please see our docs on [breaking changes](https://github.com/octokit/.github/blob/master/community/breaking_changes.md) to help!

- [ ] Yes (Please add the `Type: Breaking change` label)
- [x] No

If `Yes`, what's the impact:  

* N/A


### Pull request type

<!-- Please do not submit updates to dependencies unless it fixes an issue. --> 
<!-- Please try to limit your pull request to one type, submit multiple pull requests if needed. --> 

Please add the corresponding label for change this PR introduces:
- Bugfix: `Type: Bug`
- Feature/model/API additions: `Type: Feature`
- Updates to docs or samples: `Type: Documentation`
- Dependencies/code cleanup: `Type: Maintenance`

----



Patch:
diff --git a/src/middleware/node/middleware.ts b/src/middleware/node/middleware.ts
index f0c818a..b3be216 100644
--- a/src/middleware/node/middleware.ts
+++ b/src/middleware/node/middleware.ts
@@ -1,5 +1,5 @@
 // remove type imports from http for Deno compatibility
-// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
+// see https://github.com/octokit/octokit.js/issues/2075#issuecomment-817361886
 // import { IncomingMessage, ServerResponse } from "http";
 type IncomingMessage = any;
 type ServerResponse = any;
@@ -44,6 +44,22 @@ export async function middleware(
     }
   }
 
+  // Check if the Content-Type header is `application/json` and allow for charset to be specified in it
+  // Otherwise, return a 415 Unsupported Media Type error
+  // See https://github.com/octokit/webhooks.js/issues/158
+  if (!request.headers["content-type"].startsWith("application/json")) {
+    response.writeHead(415, {
+      "content-type": "application/json",
+      accept: "application/json",
+    });
+    response.end(
+      JSON.stringify({
+        error: `Unsupported "Content-Type" header value. Must be "application/json"`,
+      })
+    );
+    return;
+  }
+
   const missingHeaders = getMissingHeaders(request).join(", ");
 
   if (missingHeaders) {


Test patch:
diff --git a/test/integration/node-middleware.test.ts b/test/integration/node-middleware.test.ts
index 3ffeaab..ea50cbc 100644
--- a/test/integration/node-middleware.test.ts
+++ b/test/integration/node-middleware.test.ts
@@ -48,6 +48,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -92,6 +93,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -106,6 +108,37 @@ describe("createNodeMiddleware(webhooks)", () => {
     server.close();
   });
 
+  test("Handles invalid Content-Type", async () => {
+    const webhooks = new Webhooks({
+      secret: "mySecret",
+    });
+
+    const server = createServer(createNodeMiddleware(webhooks)).listen();
+
+    // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface
+    const { port } = server.address();
+    const response = await fetch(
+      `http://localhost:${port}/api/github/webhooks`,
+      {
+        method: "POST",
+        headers: {
+          "Content-Type": "text/plain",
+          "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
+          "X-GitHub-Event": "push",
+          "X-Hub-Signature-256": signatureSha256,
+        },
+        body: pushEventPayload,
+      }
+    );
+
+    await expect(response.text()).resolves.toBe(
+      '{"error":"Unsupported \\"Content-Type\\" header value. Must be \\"application/json\\""}'
+    );
+    expect(response.status).toEqual(415);
+
+    server.close();
+  });
+
   test("Handles invalid JSON", async () => {
     const webhooks = new Webhooks({
       secret: "mySecret",
@@ -121,6 +154,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -151,6 +185,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "PUT",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -183,6 +218,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           // "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -219,6 +255,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -252,6 +289,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -290,6 +328,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -325,6 +364,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,
@@ -413,6 +453,7 @@ describe("createNodeMiddleware(webhooks)", () => {
     const response = await fetch(`http://localhost:${port}/test`, {
       method: "POST",
       headers: {
+        "Content-Type": "application/json",
         "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
         "X-GitHub-Event": "push",
         "X-Hub-Signature-256": signatureSha256,
@@ -444,6 +485,7 @@ describe("createNodeMiddleware(webhooks)", () => {
     const response = await fetch(`http://localhost:${port}/test`, {
       method: "POST",
       headers: {
+        "Content-Type": "application/json",
         "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
         "X-GitHub-Event": "push",
         "X-Hub-Signature-256": signatureSha256,
@@ -484,6 +526,7 @@ describe("createNodeMiddleware(webhooks)", () => {
       {
         method: "POST",
         headers: {
+          "Content-Type": "application/json",
           "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
           "X-GitHub-Event": "push",
           "X-Hub-Signature-256": signatureSha256,