Repository: octokit/oauth-app.js (ts)

Issue:
Handle callback errors
**What’s missing?**

If an authorization error occurs, the callback redirect has different query parameters. Example

```
?error=redirect_uri_mismatch&error_description=The+redirect_uri+MUST+match+the+registered+callback+URL+for+this+application.&error_uri=https%3A%2F%2Fdeveloper.github.com%2Fapps%2Fmanaging-oauth-apps%2Ftroubleshooting-authorization-request-errors%2F%23redirect-uri-mismatch
```

**Why?**

It's a standard error behavior of GitHub's API and should be covered by Octokit

Pull request description:
Handle error responses to callback endpoint
This adds a check in middleware.ts to report an error if GitHub
sends the user back to the callback endpoint with an error query
parameter. Some potential error cases are `redirect_uri_mismatch`
for when a user visits the /oauth/authorize endpoint with a
redirect_uri query parameter that doesn't match the application's
and `application_suspended` for when the GitHub application is
suspended.

Closes #2 

Patch:
diff --git a/README.md b/README.md
index f64cca4..34d08e9 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,7 @@ const { OAuthApp, getNodeMiddleware } = require("@octokit/oauth-app");
 
 const app = new OAuthApp({
   clientId: "0123",
-  clientSecret: "0123secret"
+  clientSecret: "0123secret",
 });
 
 app.on("token", async ({ token, octokit }) => {
@@ -72,9 +72,7 @@ app.on("token", async ({ token, octokit }) => {
   console.log(`Token retrieved for ${data.login}`);
 });
 
-require("http")
-  .createServer(getNodeMiddleware(app))
-  .listen(3000);
+require("http").createServer(getNodeMiddleware(app)).listen(3000);
 // can now receive user authorization callbacks at /api/github/oauth/callback
 ```
 
@@ -180,8 +178,8 @@ new OAuthApp({
   clientId: 123,
   clientSecret: "secret",
   Octokit: Octokit.defaults({
-    baseUrl: "https://ghe.my-company.com/api/v3"
-  })
+    baseUrl: "https://ghe.my-company.com/api/v3",
+  }),
 });
 ```
 
@@ -196,7 +194,7 @@ Defaults to [`@octokit/core`](https://github.com/octokit/core.js).
 Called whenever a new OAuth access token is created for a user. It accepts two parameters, an event name and a function with one argument
 
 ```js
-app.on("token.created", async context => {
+app.on("token.created", async (context) => {
   const { data } = await context.octokit.request("GET /user");
   app.log.info(`New token created for ${data.login}`);
 });
@@ -305,7 +303,7 @@ Returns a URL string.
 ```js
 const url = app.getAuthorizationUrl({
   state: "state123",
-  scopes: ["repo"]
+  scopes: ["repo"],
 });
 ```
 
@@ -388,7 +386,7 @@ const url = app.getAuthorizationUrl({
 ```js
 const { token, scopes } = await app.createToken({
   state: "state123",
-  code: "code123"
+  code: "code123",
 });
 ```
 
@@ -525,7 +523,7 @@ Resolves with response body from ["Check a token" request](https://developer.git
 
 ```js
 const { token } = await app.resetToken({
-  token: "token123"
+  token: "token123",
 });
 // "token123" is no longer valid. Use `token` instead
 ```
@@ -565,7 +563,7 @@ Resolves with response body from ["Reset a token" request](https://developer.git
 
 ```js
 await app.deleteToken({
-  token: "token123"
+  token: "token123",
 });
 // "token123" is no longer valid.
 ```
@@ -605,7 +603,7 @@ Resolves with response body from ["Delete a token" request](https://developer.gi
 
 ```js
 await app.deleteAuthorization({
-  token: "token123"
+  token: "token123",
 });
 // "token123" is no longer valid, and no tokens can be created until the app gets re-authorized.
 ```
@@ -652,7 +650,7 @@ const {
   checkToken,
   resetToken,
   deleteToken,
-  deleteAuthorization
+  deleteAuthorization,
 } = require("@octokit/oauth-app");
 ```
 
@@ -664,7 +662,7 @@ Returns a URL string.
 const { getAuthorizationUrl } = require("@octokit/oauth-app");
 const url = getAuthorizationUrl({
   clientId: "0123",
-  state: "state123"
+  state: "state123",
 });
 ```
 
@@ -783,7 +781,7 @@ const { token, scopes } = await createToken({
   clientId: "0123",
   clientSecret: "0123secret",
   state: "state123",
-  code: "code123"
+  code: "code123",
 });
 ```
 
@@ -899,7 +897,7 @@ try {
   const { created_at, app, user } = await checkToken({
     clientId: "0123",
     clientSecret: "0123secret",
-    token: "token123"
+    token: "token123",
   });
   console.log(
     `token valid, created on %s by %s for %s`,
@@ -972,7 +970,7 @@ const { resetToken } = require("@octokit/oauth-app");
 const { token } = await resetToken({
   clientId: "0123",
   clientSecret: "0123secret",
-  token: "token123"
+  token: "token123",
 });
 // "token123" is no longer valid. Use `token` instead
 ```
@@ -1037,7 +1035,7 @@ const { deleteToken } = require("@octokit/oauth-app");
 await deleteToken({
   clientId: "0123",
   clientSecret: "0123secret",
-  token: "token123"
+  token: "token123",
 });
 // "token123" is no longer valid
 ```
@@ -1102,7 +1100,7 @@ const { deleteAuthorization } = require("@octokit/oauth-app");
 await deleteAuthorization({
   clientId: "0123",
   clientSecret: "0123secret",
-  token: "token123"
+  token: "token123",
 });
 // "token123" is no longer valid
 ```
@@ -1184,16 +1182,14 @@ Native http server middleware for Node.js
 const { OAuthApp, getNodeMiddleware } = require("@octokit/oauth-app");
 const app = new OAuthApp({
   clientId: "0123",
-  clientSecret: "0123secret"
+  clientSecret: "0123secret",
 });
 
 const middleware = getNodeMiddleware(app, {
-  pathPrefix: "/api/github/oauth/"
+  pathPrefix: "/api/github/oauth/",
 });
 
-require("http")
-  .createServer(getNodeMiddleware(app))
-  .listen(3000);
+require("http").createServer(getNodeMiddleware(app)).listen(3000);
 // can now receive user authorization callbacks at /api/github/oauth/callback
 ```
 
@@ -1249,11 +1245,11 @@ Defaults to
 ```js
 function onUnhandledRequest(request, response) {
   response.writeHead(400, {
-    "content-type": "application/json"
+    "content-type": "application/json",
   });
   response.end(
     JSON.stringify({
-      error: error.message
+      error: error.message,
     })
   );
 }
diff --git a/package-lock.json b/package-lock.json
index a74daa1..13203f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12413,9 +12413,9 @@
       "dev": true
     },
     "prettier": {
-      "version": "1.19.1",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
-      "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.0.tgz",
+      "integrity": "sha512-vI55PC+GFLOVtpwr2di1mYhJF36v+kztJov8sx3AmqbfdA+2Dhozxb+3e1hTgoV9lyhnVJFF3Z8GCVeMBOS1bA==",
       "dev": true
     },
     "pretty-format": {
diff --git a/package.json b/package.json
index bb8445f..5df810c 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
     "jest": "^25.1.0",
     "nock": "^12.0.1",
     "node-fetch": "^2.6.0",
-    "prettier": "^1.19.1",
+    "prettier": "^2.0.0",
     "semantic-release": "^17.0.3",
     "semantic-release-plugin-update-version-in-files": "^1.0.0",
     "ts-jest": "^25.2.0",
diff --git a/src/index.ts b/src/index.ts
index 5e8dd3b..69580c5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,7 +6,7 @@ import { OAuthAppOctokit } from "./oauth-app-octokit";
 
 import {
   getAuthorizationUrlWithState,
-  AppGetAuthorizationUrl
+  AppGetAuthorizationUrl,
 } from "./methods/get-authorization-url";
 import { createTokenWithState, AppCreateToken } from "./methods/create-token";
 import { checkTokenWithState, AppCheckToken } from "./methods/check-token";
@@ -14,14 +14,14 @@ import { resetTokenWithState, AppResetToken } from "./methods/reset-token";
 import { deleteTokenWithState, AppDeleteToken } from "./methods/delete-token";
 import {
   deleteAuthorizationWithState,
-  AppDeleteAuthorization
+  AppDeleteAuthorization,
 } from "./methods/delete-authorization";
 
 import {
   ConstructorOptions,
   OctokitInstance,
   AddEventHandler,
-  State
+  State,
 } from "./types";
 
 export { getAuthorizationUrl } from "./methods/get-authorization-url";
@@ -41,8 +41,8 @@ export class OAuthApp {
       authStrategy: createOAuthAppAuth,
       auth: {
         clientId: options.clientId,
-        clientSecret: options.clientSecret
-      }
+        clientSecret: options.clientSecret,
+      },
     });
 
     const state: State = {
@@ -54,7 +54,7 @@ export class OAuthApp {
       log: options.log,
       Octokit,
       octokit,
-      eventHandlers: {}
+      eventHandlers: {},
     };
 
     this.on = addEventHandler.bind(null, state);
diff --git a/src/methods/check-token.ts b/src/methods/check-token.ts
index 7bc010b..25c2a32 100644
--- a/src/methods/check-token.ts
+++ b/src/methods/check-token.ts
@@ -34,20 +34,20 @@ export async function checkToken(options: Options) {
     headers: {
       authorization: `basic ${btoa(
         `${options.clientId}:${options.clientSecret}`
-      )}`
-    }
+      )}`,
+    },
   });
 
   return sendCheckTokenRequest(request, {
     client_id: options.clientId,
-    access_token: options.token
+    access_token: options.token,
   });
 }
 
 export function checkTokenWithState(state: State, options: StateOptions) {
   return sendCheckTokenRequest(state.octokit.request, {
     client_id: state.clientId,
-    access_token: options.token
+    access_token: options.token,
   });
 }
 
diff --git a/src/methods/create-token.ts b/src/methods/create-token.ts
index a427d4a..3c40fbf 100644
--- a/src/methods/create-token.ts
+++ b/src/methods/create-token.ts
@@ -22,7 +22,7 @@ async function createTokenWithAuth(auth: Auth, options: StateOptions) {
   const { token, scopes } = await auth({
     type: "token",
     state: options.state,
-    code: options.code
+    code: options.code,
   });
 
   return { token, scopes };
@@ -32,7 +32,7 @@ export function createToken(options: Options) {
   const { clientId, clientSecret, ...otherOptions } = options;
   const auth = createOAuthAppAuth({
     clientId,
-    clientSecret
+    clientSecret,
   });
 
   return createTokenWithAuth(auth, otherOptions);
@@ -51,7 +51,7 @@ export async function createTokenWithState(
     scopes: result.scopes,
     get octokit() {
       return new state.Octokit({ auth: result.token });
-    }
+    },
   });
 
   return result;
diff --git a/src/methods/delete-authorization.ts b/src/methods/delete-authorization.ts
index b0fb0aa..e3c9902 100644
--- a/src/methods/delete-authorization.ts
+++ b/src/methods/delete-authorization.ts
@@ -35,14 +35,14 @@ export function deleteAuthorization(options: Options) {
     request: {
       hook: createOAuthAppAuth({
         clientId: options.clientId,
-        clientSecret: options.clientSecret
-      }).hook
-    }
+        clientSecret: options.clientSecret,
+      }).hook,
+    },
   });
 
   return sendDeleteAuthorizationRequest(request, {
     client_id: options.clientId,
-    access_token: options.token
+    access_token: options.token,
   });
 }
 
@@ -56,7 +56,7 @@ export async function deleteAuthorizationWithState(
     token: options.token,
     get octokit() {
       return new state.Octokit({ auth: options.token });
-    }
+    },
   });
 
   await emitEvent(state, {
@@ -65,24 +65,24 @@ export async function deleteAuthorizationWithState(
     token: options.token,
     get octokit() {
       return new state.Octokit({ auth: options.token });
-    }
+    },
   });
 
   const result = await sendDeleteAuthorizationRequest(state.octokit.request, {
     client_id: state.clientId,
-    access_token: options.token
+    access_token: options.token,
   });
 
   await emitEvent(state, {
     name: "token",
     action: "deleted",
-    token: options.token
+    token: options.token,
   });
 
   await emitEvent(state, {
     name: "authorization",
     action: "deleted",
-    token: options.token
+    token: options.token,
   });
 
   return result;
diff --git a/src/methods/delete-token.ts b/src/methods/delete-token.ts
index e24d25e..d1dfaff 100644
--- a/src/methods/delete-token.ts
+++ b/src/methods/delete-token.ts
@@ -35,13 +35,13 @@ export function deleteToken(options: Options) {
     headers: {
       authorization: `basic ${btoa(
         `${options.clientId}:${options.clientSecret}`
-      )}`
-    }
+      )}`,
+    },
   });
 
   return sendDeleteTokenRequest(request, {
     client_id: options.clientId,
-    access_token: options.token
+    access_token: options.token,
   });
 }
 
@@ -55,18 +55,18 @@ export async function deleteTokenWithState(
     token: options.token,
     get octokit() {
       return new state.Octokit({ auth: options.token });
-    }
+    },
   });
 
   const result = await sendDeleteTokenRequest(state.octokit.request, {
     client_id: state.clientId,
-    access_token: options.token
+    access_token: options.token,
   });
 
   await emitEvent(state, {
     name: "token",
     action: "deleted",
-    token: options.token
+    token: options.token,
   });
 
   return result;
diff --git a/src/methods/get-authorization-url.ts b/src/methods/get-authorization-url.ts
index 0e2d653..524ef9f 100644
--- a/src/methods/get-authorization-url.ts
+++ b/src/methods/get-authorization-url.ts
@@ -21,7 +21,7 @@ export function getAuthorizationUrlWithState(
         allowSignup: state.allowSignup,
         baseUrl: state.baseUrl,
         log: state.log,
-        scopes: state.defaultScopes
+        scopes: state.defaultScopes,
       },
       options
     )
diff --git a/src/methods/reset-token.ts b/src/methods/reset-token.ts
index d6b2e28..8d31c9c 100644
--- a/src/methods/reset-token.ts
+++ b/src/methods/reset-token.ts
@@ -35,20 +35,20 @@ export function resetToken(options: Options) {
     headers: {
       authorization: `basic ${btoa(
         `${options.clientId}:${options.clientSecret}`
-      )}`
-    }
+      )}`,
+    },
   });
 
   return sendResetTokenRequest(request, {
     client_id: options.clientId,
-    access_token: options.token
+    access_token: options.token,
   });
 }
 
 export async function resetTokenWithState(state: State, options: StateOptions) {
   const result = await sendResetTokenRequest(state.octokit.request, {
     client_id: state.clientId,
-    access_token: options.token
+    access_token: options.token,
   });
 
   await emitEvent(state, {
@@ -58,9 +58,9 @@ export async function resetTokenWithState(state: State, options: StateOptions) {
     scopes: result.scopes,
     get octokit() {
       return new state.Octokit({
-        auth: result.token
+        auth: result.token,
       });
-    }
+    },
   });
 
   return result;
diff --git a/src/middleware/node/index.ts b/src/middleware/node/index.ts
index f231b9a..0f19a3d 100644
--- a/src/middleware/node/index.ts
+++ b/src/middleware/node/index.ts
@@ -8,11 +8,11 @@ export function getNodeMiddleware(
   app: OAuthApp,
   {
     pathPrefix = "/api/github/oauth",
-    onUnhandledRequest = onUnhandledRequestDefault
+    onUnhandledRequest = onUnhandledRequestDefault,
   }: MiddlewareOptions = {}
 ) {
   return middleware.bind(null, app, {
     pathPrefix,
-    onUnhandledRequest
+    onUnhandledRequest,
   } as Required<MiddlewareOptions>);
 }
diff --git a/src/middleware/node/middleware.ts b/src/middleware/node/middleware.ts
index 3005fc5..b439126 100644
--- a/src/middleware/node/middleware.ts
+++ b/src/middleware/node/middleware.ts
@@ -16,11 +16,11 @@ export async function middleware(
     parsedRequest = await parseRequest(request);
   } catch (error) {
     response.writeHead(400, {
-      "content-type": "application/json"
+      "content-type": "application/json",
     });
     return response.end(
       JSON.stringify({
-        error: "[@octokit/oauth-app] request error"
+        error: "[@octokit/oauth-app] request error",
       })
     );
   }
@@ -32,7 +32,7 @@ export async function middleware(
         state: query.state,
         scopes: query.scopes?.split(","),
         allowSignup: query.allowSignup,
-        redirectUrl: query.redirectUrl
+        redirectUrl: query.redirectUrl,
       });
 
       response.writeHead(302, { location: url });
@@ -40,6 +40,11 @@ export async function middleware(
     }
 
     if (route === `GET ${options.pathPrefix}/callback`) {
+      if (query.error) {
+        throw new Error(
+          `[@octokit/oauth-app] ${query.error} ${query.error_description}`
+        );
+      }
       if (!query.state || !query.code) {
         throw new Error(
           '[@octokit/oauth-app] Both "code" & "state" parameters are required'
@@ -48,11 +53,11 @@ export async function middleware(
 
       const { token } = await app.createToken({
         state: query.state,
-        code: query.code
+        code: query.code,
       });
 
       response.writeHead(200, {
-        "content-type": "text/html"
+        "content-type": "text/html",
       });
       response.write(`<h1>Token created successfull</h1>
     
@@ -73,11 +78,11 @@ export async function middleware(
 
       const { token, scopes } = await app.createToken({
         state: oauthState,
-        code
+        code,
       });
 
       response.writeHead(201, {
-        "content-type": "application/json"
+        "content-type": "application/json",
       });
 
       return response.end(JSON.stringify({ token, scopes }));
@@ -93,11 +98,11 @@ export async function middleware(
       }
 
       const result = await app.checkToken({
-        token
+        token,
       });
 
       response.writeHead(200, {
-        "content-type": "application/json"
+        "content-type": "application/json",
       });
       return response.end(JSON.stringify(result));
     }
@@ -112,11 +117,11 @@ export async function middleware(
       }
 
       const result = await app.resetToken({
-        token
+        token,
       });
 
       response.writeHead(200, {
-        "content-type": "application/json"
+        "content-type": "application/json",
       });
       return response.end(JSON.stringify(result));
     }
@@ -131,7 +136,7 @@ export async function middleware(
       }
 
       await app.deleteToken({
-        token
+        token,
       });
 
       response.writeHead(204);
@@ -148,7 +153,7 @@ export async function middleware(
       }
 
       await app.deleteAuthorization({
-        token
+        token,
       });
 
       response.writeHead(204);
@@ -158,11 +163,11 @@ export async function middleware(
     options.onUnhandledRequest(request, response);
   } catch (error) {
     response.writeHead(400, {
-      "content-type": "application/json"
+      "content-type": "application/json",
     });
     response.end(
       JSON.stringify({
-        error: error.message
+        error: error.message,
       })
     );
   }
diff --git a/src/middleware/node/on-unhandled-request-default.ts b/src/middleware/node/on-unhandled-request-default.ts
index 1d97257..c32ef7a 100644
--- a/src/middleware/node/on-unhandled-request-default.ts
+++ b/src/middleware/node/on-unhandled-request-default.ts
@@ -5,11 +5,11 @@ export function onUnhandledRequestDefault(
   response: ServerResponse
 ) {
   response.writeHead(404, {
-    "content-type": "application/json"
+    "content-type": "application/json",
   });
   response.end(
     JSON.stringify({
-      error: `Unknown route: ${request.method} ${request.url}`
+      error: `Unknown route: ${request.method} ${request.url}`,
     })
   );
 }
diff --git a/src/middleware/node/parse-request.ts b/src/middleware/node/parse-request.ts
index 00e677c..7baa03b 100644
--- a/src/middleware/node/parse-request.ts
+++ b/src/middleware/node/parse-request.ts
@@ -14,6 +14,9 @@ type ParsedRequest = {
     code?: string;
     redirectUrl?: string;
     allowSignup?: boolean;
+    error?: string;
+    error_description?: string;
+    error_url?: string;
   };
   body?: {
     code?: string;
@@ -42,7 +45,7 @@ export async function parseRequest(
     let bodyChunks: Uint8Array[] = [];
     request
       .on("error", reject)
-      .on("data", chunk => bodyChunks.push(chunk))
+      .on("data", (chunk) => bodyChunks.push(chunk))
       .on("end", async () => {
         const bodyString = Buffer.concat(bodyChunks).toString();
         if (!bodyString) return resolve({ route, headers, query });
diff --git a/src/oauth-app-octokit.ts b/src/oauth-app-octokit.ts
index 98059ea..fdd46db 100644
--- a/src/oauth-app-octokit.ts
+++ b/src/oauth-app-octokit.ts
@@ -3,5 +3,5 @@ import { getUserAgent } from "universal-user-agent";
 import { VERSION } from "./version";
 
 export const OAuthAppOctokit = Octokit.defaults({
-  userAgent: `octokit-oauth-app.js/${VERSION} ${getUserAgent()}`
+  userAgent: `octokit-oauth-app.js/${VERSION} ${getUserAgent()}`,
 });


Test patch:
diff --git a/test/app.test.ts b/test/app.test.ts
index f72b989..55ef4e7 100644
--- a/test/app.test.ts
+++ b/test/app.test.ts
@@ -7,10 +7,10 @@ describe("app", () => {
   it("app.getAuthorizationUrl(options)", () => {
     const app = new OAuthApp({
       clientId: "0123",
-      clientSecret: "0123secret"
+      clientSecret: "0123secret",
     });
     const url = app.getAuthorizationUrl({
-      state: "state123"
+      state: "state123",
     });
     expect(url).toStrictEqual(
       "https://github.com/login/oauth/authorize?allow_signup=true&client_id=0123&state=state123"
@@ -25,15 +25,15 @@ describe("app", () => {
         {
           access_token: "token123",
           scope: "repo,gist",
-          token_type: "bearer"
+          token_type: "bearer",
         },
         {
           body: {
             client_id: "0123",
             client_secret: "0123secret",
             code: "code123",
-            state: "state123"
-          }
+            state: "state123",
+          },
         }
       )
       .getOnce(
@@ -41,21 +41,21 @@ describe("app", () => {
         { login: "octocat" },
         {
           headers: {
-            authorization: "token token123"
-          }
+            authorization: "token token123",
+          },
         }
       );
 
     const Mocktokit = OAuthAppOctokit.defaults({
       request: {
-        fetch: mock
-      }
+        fetch: mock,
+      },
     });
 
     const app = new OAuthApp({
       clientId: "0123",
       clientSecret: "0123secret",
-      Octokit: Mocktokit
+      Octokit: Mocktokit,
     });
 
     const onTokenCallback = jest.fn();
@@ -63,7 +63,7 @@ describe("app", () => {
 
     const { token, scopes } = await app.createToken({
       state: "state123",
-      code: "code123"
+      code: "code123",
     });
 
     expect(token).toEqual("token123");
@@ -74,7 +74,7 @@ describe("app", () => {
 
     expect(context).toMatchObject({
       token: "token123",
-      scopes: ["repo", "gist"]
+      scopes: ["repo", "gist"],
     });
     expect(context.octokit).toBeInstanceOf(Mocktokit);
 
@@ -89,28 +89,28 @@ describe("app", () => {
       {
         headers: {
           authorization:
-            "basic " + Buffer.from("0123:0123secret").toString("base64")
+            "basic " + Buffer.from("0123:0123secret").toString("base64"),
         },
         body: {
-          access_token: "token123"
-        }
+          access_token: "token123",
+        },
       }
     );
 
     const Mocktokit = OAuthAppOctokit.defaults({
       request: {
-        fetch: mock
-      }
+        fetch: mock,
+      },
     });
 
     const app = new OAuthApp({
       clientId: "0123",
       clientSecret: "0123secret",
-      Octokit: Mocktokit
+      Octokit: Mocktokit,
     });
 
     const result = await app.checkToken({
-      token: "token123"
+      token: "token123",
     });
 
     expect(result).toStrictEqual({ id: 1 });
@@ -122,42 +122,42 @@ describe("app", () => {
       {
         id: 2,
         token: "token456",
-        scopes: ["repo", "gist"]
+        scopes: ["repo", "gist"],
       },
       {
         headers: {
           authorization:
-            "basic " + Buffer.from("0123:0123secret").toString("base64")
+            "basic " + Buffer.from("0123:0123secret").toString("base64"),
         },
         body: {
-          access_token: "token123"
-        }
+          access_token: "token123",
+        },
       }
     );
 
     const Mocktokit = OAuthAppOctokit.defaults({
       request: {
-        fetch: mock
-      }
+        fetch: mock,
+      },
     });
 
     const app = new OAuthApp({
       clientId: "0123",
       clientSecret: "0123secret",
-      Octokit: Mocktokit
+      Octokit: Mocktokit,
     });
 
     const onTokenCallback = jest.fn();
     app.on("token.reset", onTokenCallback);
 
     const result = await app.resetToken({
-      token: "token123"
+      token: "token123",
     });
 
     expect(result).toStrictEqual({
       id: 2,
       token: "token456",
-      scopes: ["repo", "gist"]
+      scopes: ["repo", "gist"],
     });
     expect(onTokenCallback.mock.calls.length).toEqual(1);
     const [context] = onTokenCallback.mock.calls[0];
@@ -166,7 +166,7 @@ describe("app", () => {
       name: "token",
       action: "reset",
       token: "token456",
-      scopes: ["repo", "gist"]
+      scopes: ["repo", "gist"],
     });
     expect(context.octokit).toBeInstanceOf(Mocktokit);
   });
@@ -177,30 +177,30 @@ describe("app", () => {
       .deleteOnce("https://api.github.com/applications/0123/token", 204, {
         headers: {
           authorization:
-            "basic " + Buffer.from("0123:0123secret").toString("base64")
+            "basic " + Buffer.from("0123:0123secret").toString("base64"),
         },
         body: {
-          access_token: "token123"
-        }
+          access_token: "token123",
+        },
       });
 
     const Mocktokit = OAuthAppOctokit.defaults({
       request: {
-        fetch: mock
-      }
+        fetch: mock,
+      },
     });
 
     const app = new OAuthApp({
       clientId: "0123",
       clientSecret: "0123secret",
-      Octokit: Mocktokit
+      Octokit: Mocktokit,
     });
 
     const onTokenCallback = jest.fn();
     app.on(["token.before_deleted", "token.deleted"], onTokenCallback);
 
     const result = await app.deleteToken({
-      token: "token123"
+      token: "token123",
     });
 
     expect(result).toBeUndefined();
@@ -212,14 +212,14 @@ describe("app", () => {
     expect(context_before_deleted).toMatchObject({
       name: "token",
       action: "before_deleted",
-      token: "token123"
+      token: "token123",
     });
     expect(context_before_deleted.octokit).toBeInstanceOf(Mocktokit);
 
     expect(context_deleted).toStrictEqual({
       name: "token",
       action: "deleted",
-      token: "token123"
+      token: "token123",
     });
   });
 
@@ -229,37 +229,37 @@ describe("app", () => {
       .deleteOnce("https://api.github.com/applications/0123/grant", 204, {
         headers: {
           authorization:
-            "basic " + Buffer.from("0123:0123secret").toString("base64")
+            "basic " + Buffer.from("0123:0123secret").toString("base64"),
         },
         body: {
-          access_token: "token123"
-        }
+          access_token: "token123",
+        },
       });
 
     const Mocktokit = OAuthAppOctokit.defaults({
       request: {
-        fetch: mock
-      }
+        fetch: mock,
+      },
     });
 
     const app = new OAuthApp({
       clientId: "0123",
       clientSecret: "0123secret",
-      Octokit: Mocktokit
+      Octokit: Mocktokit,
     });
 
     const onTokenCallback = jest.fn();
     app.on(["token", "authorization"], onTokenCallback);
 
     const result = await app.deleteAuthorization({
-      token: "token123"
+      token: "token123",
     });
 
     expect(result).toBeUndefined();
 
     expect(onTokenCallback.mock.calls.length).toEqual(4);
     const [
-      context_authorization_before_deleted
+      context_authorization_before_deleted,
     ] = onTokenCallback.mock.calls[0];
     const [context_token_before_deleted] = onTokenCallback.mock.calls[1];
     const [context_token_deleted] = onTokenCallback.mock.calls[2];
@@ -268,7 +268,7 @@ describe("app", () => {
     expect(context_authorization_before_deleted).toMatchObject({
       name: "authorization",
       action: "before_deleted",
-      token: "token123"
+      token: "token123",
     });
     expect(context_authorization_before_deleted.octokit).toBeInstanceOf(
       Mocktokit
@@ -277,19 +277,19 @@ describe("app", () => {
     expect(context_token_before_deleted).toMatchObject({
       name: "token",
       action: "before_deleted",
-      token: "token123"
+      token: "token123",
     });
     expect(context_token_before_deleted.octokit).toBeInstanceOf(Mocktokit);
 
     expect(context_token_deleted).toStrictEqual({
       name: "token",
       action: "deleted",
-      token: "token123"
+      token: "token123",
     });
     expect(context_authorization_deleted).toStrictEqual({
       name: "authorization",
       action: "deleted",
-      token: "token123"
+      token: "token123",
     });
   });
 
@@ -301,15 +301,15 @@ describe("app", () => {
         {
           access_token: "token123",
           scope: "repo,gist",
-          token_type: "bearer"
+          token_type: "bearer",
         },
         {
           body: {
             client_id: "0123",
             client_secret: "0123secret",
             code: "code123",
-            state: "state123"
-          }
+            state: "state123",
+          },
         }
       )
       .getOnce(
@@ -317,40 +317,40 @@ describe("app", () => {
         { login: "octocat" },
         {
           headers: {
-            authorization: "token token123"
-          }
+            authorization: "token token123",
+          },
         }
       );
 
     const Mocktokit = OAuthAppOctokit.defaults({
       request: {
-        fetch: mock
-      }
+        fetch: mock,
+      },
     });
 
     const app = new OAuthApp({
       clientId: "0123",
       clientSecret: "0123secret",
-      Octokit: Mocktokit
+      Octokit: Mocktokit,
     });
 
     const calls: number[] = [];
 
-    const onTokenCallback1 = jest.fn().mockImplementationOnce(context => {
+    const onTokenCallback1 = jest.fn().mockImplementationOnce((context) => {
       calls.push(1);
-      return new Promise(resolve => setTimeout(resolve, 20));
+      return new Promise((resolve) => setTimeout(resolve, 20));
     });
 
-    const onTokenCallback2 = jest.fn().mockImplementationOnce(context => {
+    const onTokenCallback2 = jest.fn().mockImplementationOnce((context) => {
       calls.push(2);
-      return new Promise(resolve => setTimeout(resolve, 10));
+      return new Promise((resolve) => setTimeout(resolve, 10));
     });
     app.on("token.created", onTokenCallback1);
     app.on("token.created", onTokenCallback2);
 
     await app.createToken({
       state: "state123",
-      code: "code123"
+      code: "code123",
     });
 
     expect(calls).toStrictEqual([1, 2]);
diff --git a/test/methods.test.ts b/test/methods.test.ts
index fa3963b..e305175 100644
--- a/test/methods.test.ts
+++ b/test/methods.test.ts
@@ -6,7 +6,7 @@ import {
   checkToken,
   resetToken,
   deleteToken,
-  deleteAuthorization
+  deleteAuthorization,
 } from "../src";
 
 nock.disableNetConnect();
@@ -15,7 +15,7 @@ describe("app", () => {
   it("getAuthorizationUrl(options)", () => {
     const url = getAuthorizationUrl({
       clientId: "0123",
-      state: "state123"
+      state: "state123",
     });
     expect(url).toStrictEqual(
       "https://github.com/login/oauth/authorize?allow_signup=true&client_id=0123&state=state123"
@@ -23,19 +23,17 @@ describe("app", () => {
   });
 
   it("createToken(options)", async () => {
-    nock("https://github.com")
-      .post("/login/oauth/access_token")
-      .reply(200, {
-        access_token: "token123",
-        scope: "repo,gist",
-        token_type: "bearer"
-      });
+    nock("https://github.com").post("/login/oauth/access_token").reply(200, {
+      access_token: "token123",
+      scope: "repo,gist",
+      token_type: "bearer",
+    });
 
     const { token, scopes } = await createToken({
       clientId: "0123",
       clientSecret: "0123secret",
       state: "state123",
-      code: "code123"
+      code: "code123",
     });
 
     expect(token).toEqual("token123");
@@ -45,18 +43,18 @@ describe("app", () => {
   it("checkToken(options)", async () => {
     nock("https://api.github.com", {
       reqheaders: {
-        authorization: "basic MDEyMzowMTIzc2VjcmV0"
-      }
+        authorization: "basic MDEyMzowMTIzc2VjcmV0",
+      },
     })
       .post("/applications/0123/token", {
-        access_token: "token123"
+        access_token: "token123",
       })
       .reply(200, { ok: true });
 
     const result = await checkToken({
       clientId: "0123",
       clientSecret: "0123secret",
-      token: "token123"
+      token: "token123",
     });
 
     expect(result).toStrictEqual({ ok: true });
@@ -65,18 +63,18 @@ describe("app", () => {
   it("resetToken(options)", async () => {
     nock("https://api.github.com", {
       reqheaders: {
-        authorization: "basic MDEyMzowMTIzc2VjcmV0"
-      }
+        authorization: "basic MDEyMzowMTIzc2VjcmV0",
+      },
     })
       .patch("/applications/0123/token", {
-        access_token: "token123"
+        access_token: "token123",
       })
       .reply(200, { ok: true });
 
     const result = await resetToken({
       clientId: "0123",
       clientSecret: "0123secret",
-      token: "token123"
+      token: "token123",
     });
 
     expect(result).toStrictEqual({ ok: true });
@@ -85,32 +83,32 @@ describe("app", () => {
   it("deleteToken(options)", async () => {
     nock("https://api.github.com", {
       reqheaders: {
-        authorization: "basic MDEyMzowMTIzc2VjcmV0"
-      }
+        authorization: "basic MDEyMzowMTIzc2VjcmV0",
+      },
     })
       .delete("/applications/0123/token", {
-        access_token: "token123"
+        access_token: "token123",
       })
       .reply(204);
 
     await deleteToken({
       clientId: "0123",
       clientSecret: "0123secret",
-      token: "token123"
+      token: "token123",
     });
   });
 
   it("deleteAuthorization(options)", async () => {
     nock("https://api.github.com")
       .delete("/applications/0123/grant", {
-        access_token: "token123"
+        access_token: "token123",
       })
       .reply(204);
 
     await deleteAuthorization({
       clientId: "0123",
       clientSecret: "0123secret",
-      token: "token123"
+      token: "token123",
     });
   });
 });
diff --git a/test/node-middleware.test.ts b/test/node-middleware.test.ts
index 2c35ae2..ecfb3ef 100644
--- a/test/node-middleware.test.ts
+++ b/test/node-middleware.test.ts
@@ -8,7 +8,7 @@ describe("getNodeMiddleware(app)", () => {
   it("GET /api/github/oauth/login", async () => {
     const app = new OAuthApp({
       clientId: "0123",
-      clientSecret: "0123secret"
+      clientSecret: "0123secret",
     });
 
     const server = createServer(getNodeMiddleware(app)).listen();
@@ -18,7 +18,7 @@ describe("getNodeMiddleware(app)", () => {
     const { status, headers } = await fetch(
       `http://localhost:${port}/api/github/oauth/login`,
       {
-        redirect: "manual"
+        redirect: "manual",
       }
     );
 
@@ -29,7 +29,7 @@ describe("getNodeMiddleware(app)", () => {
     const url = new URL(headers.get("location") as string);
     expect(url).toMatchObject({
       origin: "https://github.com",
-      pathname: "/login/oauth/authorize"
+      pathname: "/login/oauth/authorize",
     });
     expect(url.searchParams.get("client_id")).toEqual("0123");
     expect(url.searchParams.get("state")).toMatch(/^\w+$/);
@@ -39,7 +39,7 @@ describe("getNodeMiddleware(app)", () => {
   it("GET /api/github/oauth/login?state=mystate123&scopes=one,two,three", async () => {
     const app = new OAuthApp({
       clientId: "0123",
-      clientSecret: "0123secret"
+      clientSecret: "0123secret",
     });
 
     const server = createServer(getNodeMiddleware(app)).listen();
@@ -49,7 +49,7 @@ describe("getNodeMiddleware(app)", () => {
     const { status, headers } = await fetch(
       `http://localhost:${port}/api/github/oauth/login?state=mystate123&scopes=one,two,three`,
       {
-        redirect: "manual"
+        redirect: "manual",
       }
     );
 
@@ -60,7 +60,7 @@ describe("getNodeMiddleware(app)", () => {
     const url = new URL(headers.get("location") as string);
     expect(url).toMatchObject({
       origin: "https://github.com",
-      pathname: "/login/oauth/authorize"
+      pathname: "/login/oauth/authorize",
     });
 
     expect(url.searchParams.get("client_id")).toEqual("0123");
@@ -70,7 +70,7 @@ describe("getNodeMiddleware(app)", () => {
 
   it("GET /api/github/oauth/callback?code=012345&state=mystate123", async () => {
     const appMock = {
-      createToken: jest.fn().mockResolvedValue({ token: "token123" })
+      createToken: jest.fn().mockResolvedValue({ token: "token123" }),
     };
 
     const server = createServer(
@@ -91,7 +91,7 @@ describe("getNodeMiddleware(app)", () => {
     expect(appMock.createToken.mock.calls.length).toEqual(1);
     expect(appMock.createToken.mock.calls[0][0]).toStrictEqual({
       state: "state123",
-      code: "012345"
+      code: "012345",
     });
   });
 
@@ -99,7 +99,7 @@ describe("getNodeMiddleware(app)", () => {
     const appMock = {
       createToken: jest
         .fn()
-        .mockResolvedValue({ token: "token123", scopes: ["repo", "gist"] })
+        .mockResolvedValue({ token: "token123", scopes: ["repo", "gist"] }),
     };
 
     const server = createServer(
@@ -114,8 +114,8 @@ describe("getNodeMiddleware(app)", () => {
         method: "POST",
         body: JSON.stringify({
           code: "012345",
-          state: "state123"
-        })
+          state: "state123",
+        }),
       }
     );
 
@@ -124,19 +124,19 @@ describe("getNodeMiddleware(app)", () => {
     expect(response.status).toEqual(201);
     expect(await response.json()).toStrictEqual({
       token: "token123",
-      scopes: ["repo", "gist"]
+      scopes: ["repo", "gist"],
     });
 
     expect(appMock.createToken.mock.calls.length).toEqual(1);
     expect(appMock.createToken.mock.calls[0][0]).toStrictEqual({
       state: "state123",
-      code: "012345"
+      code: "012345",
     });
   });
 
   it("GET /api/github/oauth/token", async () => {
     const appMock = {
-      checkToken: jest.fn().mockResolvedValue({ id: 1 })
+      checkToken: jest.fn().mockResolvedValue({ id: 1 }),
     };
 
     const server = createServer(
@@ -149,8 +149,8 @@ describe("getNodeMiddleware(app)", () => {
       `http://localhost:${port}/api/github/oauth/token`,
       {
         headers: {
-          authorization: "token token123"
-        }
+          authorization: "token token123",
+        },
       }
     );
 
@@ -161,7 +161,7 @@ describe("getNodeMiddleware(app)", () => {
 
     expect(appMock.checkToken.mock.calls.length).toEqual(1);
     expect(appMock.checkToken.mock.calls[0][0]).toStrictEqual({
-      token: "token123"
+      token: "token123",
     });
   });
 
@@ -170,8 +170,8 @@ describe("getNodeMiddleware(app)", () => {
       resetToken: jest.fn().mockResolvedValue({
         id: 2,
         token: "token456",
-        scopes: ["repo", "gist"]
-      })
+        scopes: ["repo", "gist"],
+      }),
     };
 
     const server = createServer(
@@ -185,8 +185,8 @@ describe("getNodeMiddleware(app)", () => {
       {
         method: "PATCH",
         headers: {
-          authorization: "token token123"
-        }
+          authorization: "token token123",
+        },
       }
     );
 
@@ -196,18 +196,18 @@ describe("getNodeMiddleware(app)", () => {
     expect(await response.json()).toStrictEqual({
       id: 2,
       token: "token456",
-      scopes: ["repo", "gist"]
+      scopes: ["repo", "gist"],
     });
 
     expect(appMock.resetToken.mock.calls.length).toEqual(1);
     expect(appMock.resetToken.mock.calls[0][0]).toStrictEqual({
-      token: "token123"
+      token: "token123",
     });
   });
 
   it("DELETE /api/github/oauth/token", async () => {
     const appMock = {
-      deleteToken: jest.fn().mockResolvedValue(undefined)
+      deleteToken: jest.fn().mockResolvedValue(undefined),
     };
 
     const server = createServer(
@@ -221,8 +221,8 @@ describe("getNodeMiddleware(app)", () => {
       {
         method: "DELETE",
         headers: {
-          authorization: "token token123"
-        }
+          authorization: "token token123",
+        },
       }
     );
 
@@ -232,13 +232,13 @@ describe("getNodeMiddleware(app)", () => {
 
     expect(appMock.deleteToken.mock.calls.length).toEqual(1);
     expect(appMock.deleteToken.mock.calls[0][0]).toStrictEqual({
-      token: "token123"
+      token: "token123",
     });
   });
 
   it("DELETE /api/github/oauth/grant", async () => {
     const appMock = {
-      deleteAuthorization: jest.fn().mockResolvedValue(undefined)
+      deleteAuthorization: jest.fn().mockResolvedValue(undefined),
     };
 
     const server = createServer(
@@ -252,8 +252,8 @@ describe("getNodeMiddleware(app)", () => {
       {
         method: "DELETE",
         headers: {
-          authorization: "token token123"
-        }
+          authorization: "token token123",
+        },
       }
     );
 
@@ -263,7 +263,7 @@ describe("getNodeMiddleware(app)", () => {
 
     expect(appMock.deleteAuthorization.mock.calls.length).toEqual(1);
     expect(appMock.deleteAuthorization.mock.calls[0][0]).toStrictEqual({
-      token: "token123"
+      token: "token123",
     });
   });
 
@@ -303,7 +303,29 @@ describe("getNodeMiddleware(app)", () => {
     expect(response.status).toEqual(400);
     expect(await response.json()).toStrictEqual({
       error:
-        '[@octokit/oauth-app] Both "code" & "state" parameters are required'
+        '[@octokit/oauth-app] Both "code" & "state" parameters are required',
+    });
+  });
+
+  it("GET /api/github/oauth/callback with error", async () => {
+    const appMock = {};
+
+    const server = createServer(
+      getNodeMiddleware((appMock as unknown) as OAuthApp)
+    ).listen();
+    // @ts-ignore complains about { port } although it's included in returned AddressInfo interface
+    const { port } = server.address();
+
+    const response = await fetch(
+      `http://localhost:${port}/api/github/oauth/callback?error=redirect_uri_mismatch&error_description=The+redirect_uri+MUST+match+the+registered+callback+URL+for+this+application.&error_uri=https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/troubleshooting-authorization-request-errors/%23redirect-uri-mismatch&state=xyz`
+    );
+
+    server.close();
+
+    expect(response.status).toEqual(400);
+    expect(await response.json()).toStrictEqual({
+      error:
+        "[@octokit/oauth-app] redirect_uri_mismatch The redirect_uri MUST match the registered callback URL for this application."
     });
   });
 
@@ -320,7 +342,7 @@ describe("getNodeMiddleware(app)", () => {
       `http://localhost:${port}/api/github/oauth/token`,
       {
         method: "POST",
-        body: JSON.stringify({})
+        body: JSON.stringify({}),
       }
     );
 
@@ -329,7 +351,7 @@ describe("getNodeMiddleware(app)", () => {
     expect(response.status).toEqual(400);
     expect(await response.json()).toStrictEqual({
       error:
-        '[@octokit/oauth-app] Both "code" & "state" parameters are required'
+        '[@octokit/oauth-app] Both "code" & "state" parameters are required',
     });
   });
 
@@ -346,7 +368,7 @@ describe("getNodeMiddleware(app)", () => {
       `http://localhost:${port}/api/github/oauth/token`,
       {
         method: "POST",
-        body: "foo"
+        body: "foo",
       }
     );
 
@@ -354,7 +376,7 @@ describe("getNodeMiddleware(app)", () => {
 
     expect(response.status).toEqual(400);
     expect(await response.json()).toStrictEqual({
-      error: "[@octokit/oauth-app] request error"
+      error: "[@octokit/oauth-app] request error",
     });
   });
 
@@ -370,7 +392,7 @@ describe("getNodeMiddleware(app)", () => {
     const response = await fetch(
       `http://localhost:${port}/api/github/oauth/token`,
       {
-        headers: {}
+        headers: {},
       }
     );
 
@@ -378,7 +400,7 @@ describe("getNodeMiddleware(app)", () => {
 
     expect(response.status).toEqual(400);
     expect(await response.json()).toStrictEqual({
-      error: '[@octokit/oauth-app] "Authorization" header is required'
+      error: '[@octokit/oauth-app] "Authorization" header is required',
     });
   });
 
@@ -395,7 +417,7 @@ describe("getNodeMiddleware(app)", () => {
       `http://localhost:${port}/api/github/oauth/token`,
       {
         method: "PATCH",
-        headers: {}
+        headers: {},
       }
     );
 
@@ -403,7 +425,7 @@ describe("getNodeMiddleware(app)", () => {
 
     expect(response.status).toEqual(400);
     expect(await response.json()).toStrictEqual({
-      error: '[@octokit/oauth-app] "Authorization" header is required'
+      error: '[@octokit/oauth-app] "Authorization" header is required',
     });
   });
 
@@ -420,7 +442,7 @@ describe("getNodeMiddleware(app)", () => {
       `http://localhost:${port}/api/github/oauth/token`,
       {
         method: "DELETE",
-        headers: {}
+        headers: {},
       }
     );
 
@@ -428,7 +450,7 @@ describe("getNodeMiddleware(app)", () => {
 
     expect(response.status).toEqual(400);
     expect(await response.json()).toStrictEqual({
-      error: '[@octokit/oauth-app] "Authorization" header is required'
+      error: '[@octokit/oauth-app] "Authorization" header is required',
     });
   });
 
@@ -445,7 +467,7 @@ describe("getNodeMiddleware(app)", () => {
       `http://localhost:${port}/api/github/oauth/grant`,
       {
         method: "DELETE",
-        headers: {}
+        headers: {},
       }
     );
 
@@ -453,7 +475,7 @@ describe("getNodeMiddleware(app)", () => {
 
     expect(response.status).toEqual(400);
     expect(await response.json()).toStrictEqual({
-      error: '[@octokit/oauth-app] "Authorization" header is required'
+      error: '[@octokit/oauth-app] "Authorization" header is required',
     });
   });
 });