Skip to content

Commit

Permalink
fix: Error messaging from failed login would dump a JSON.parse erro…
Browse files Browse the repository at this point in the history
…r in some situations. Added a fallback if `.json` fails to parse (#794)

it will attempt `.text()` then throw result. If both attempts to parse fail it will throw an `UnknownError` with a message showing where
it originated.

should resolve #539
  • Loading branch information
JacobMGEvans authored Apr 20, 2022
1 parent 4a00910 commit ee3475f
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 2 deletions.
9 changes: 9 additions & 0 deletions .changeset/early-ducks-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"wrangler": patch
---

fix: Error messaging from failed login would dump a `JSON.parse` error in some situations. Added a fallback if `.json` fails to parse
it will attempt `.text()` then throw result. If both attempts to parse fail it will throw an `UnknownError` with a message showing where
it originated.

resolves #539
45 changes: 45 additions & 0 deletions packages/wrangler/src/__tests__/helpers/mock-oauth-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,56 @@ export const mockOAuthFlow = () => {
return outcome;
};

const mockExchangeRefreshTokenForAccessToken = ({
respondWith,
}: {
respondWith: "refreshSuccess" | "refreshError" | "badResponse";
}) => {
fetchMock.mockOnceIf(
"https://dash.cloudflare.com/oauth2/token",
async () => {
switch (respondWith) {
case "refreshSuccess":
return {
status: 200,
body: JSON.stringify({
access_token: "access_token_success_mock",
expires_in: 1701,
refresh_token: "refresh_token_sucess_mock",
scope: "scope_success_mock",
token_type: "bearer",
}),
};
case "refreshError":
return {
status: 400,
body: JSON.stringify({
error: "invalid_request",
error_description: "error_description_mock",
error_hint: "error_hint_mock",
error_verbose: "error_verbose_mock",
status_code: 400,
}),
};
case "badResponse":
return {
status: 400,
body: `<html> <body> This shouldn't be sent, but should be handled </body> </html>`,
};

default:
return "Not a respondWith option for `mockExchangeRefreshTokenForAccessToken`";
}
}
);
};

return {
mockOAuthServerCallback,
mockGrantAuthorization,
mockRevokeAuthorization,
mockGrantAccessToken,
mockExchangeRefreshTokenForAccessToken,
};
};

Expand Down
42 changes: 41 additions & 1 deletion packages/wrangler/src/__tests__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@ import path from "node:path";
import fetchMock from "jest-fetch-mock";
import {
readAuthConfigFile,
requireAuth,
USER_AUTH_CONFIG_FILE,
writeAuthConfigFile,
} from "../user";
import { mockConsoleMethods } from "./helpers/mock-console";
import { mockOAuthFlow } from "./helpers/mock-oauth-flow";
import { runInTempDir } from "./helpers/run-in-tmp";
import { runWrangler } from "./helpers/run-wrangler";
import type { Config } from "../config";
import type { UserAuthConfig } from "../user";

describe("wrangler", () => {
describe("User", () => {
runInTempDir({ homedir: "./home" });
const std = mockConsoleMethods();
const {
mockOAuthServerCallback,
mockGrantAccessToken,
mockGrantAuthorization,
mockRevokeAuthorization,
mockExchangeRefreshTokenForAccessToken,
} = mockOAuthFlow();

describe("login", () => {
Expand Down Expand Up @@ -82,4 +85,41 @@ describe("wrangler", () => {
expect(fs.existsSync(config)).toBeFalsy();
});
});

// TODO: Improve OAuth mocking to handle `/token` endpoints from different calls
it("should report error message for failed token refresh", async () => {
mockOAuthServerCallback();
writeAuthConfigFile({
oauth_token: "hunter2",
refresh_token: "Order 66",
});
mockGrantAuthorization({ respondWith: "success" });

mockExchangeRefreshTokenForAccessToken({
respondWith: "badResponse",
});

// Handles the requireAuth error throw from failed login that is unhandled due to directly calling it here
await expect(requireAuth({} as Config, false)).rejects.toThrowError();
expect(std.err).toContain(
`Error: <html> <body> This shouldn't be sent, but should be handled </body> </html>`
);
});

it("should confirm no error message when refresh is successful", async () => {
mockOAuthServerCallback();
writeAuthConfigFile({
oauth_token: "hunter2",
refresh_token: "Order 66",
});
mockGrantAuthorization({ respondWith: "success" });

mockExchangeRefreshTokenForAccessToken({
respondWith: "refreshSuccess",
});

// Handles the requireAuth error throw from failed login that is unhandled due to directly calling it here
await expect(requireAuth({} as Config, false)).rejects.toThrowError();
expect(std.err).toContain("");
});
});
21 changes: 20 additions & 1 deletion packages/wrangler/src/user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -661,8 +661,27 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
"Content-Type": "application/x-www-form-urlencoded",
},
});

if (response.status >= 400) {
throw await response.json();
let tokenExchangeResErr = undefined;

try {
tokenExchangeResErr = await response.text();
tokenExchangeResErr = JSON.parse(tokenExchangeResErr);
} catch (e) {
// If it can't parse to JSON ignore the error
}

if (tokenExchangeResErr !== undefined) {
// We will throw the parsed error if it parsed correctly, otherwise we throw an unknown error.
throw typeof tokenExchangeResErr === "string"
? new Error(tokenExchangeResErr)
: tokenExchangeResErr;
} else {
throw new ErrorUnknown(
"Failed to parse Error from exchangeRefreshTokenForAccessToken"
);
}
} else {
try {
const json = (await response.json()) as TokenResponse;
Expand Down

0 comments on commit ee3475f

Please sign in to comment.