Skip to content

Commit ee3475f

Browse files
authored
fix: Error messaging from failed login would dump a JSON.parse error 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
1 parent 4a00910 commit ee3475f

File tree

4 files changed

+115
-2
lines changed

4 files changed

+115
-2
lines changed

.changeset/early-ducks-travel.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix: Error messaging from failed login would dump a `JSON.parse` error in some situations. Added a fallback if `.json` fails to parse
6+
it will attempt `.text()` then throw result. If both attempts to parse fail it will throw an `UnknownError` with a message showing where
7+
it originated.
8+
9+
resolves #539

packages/wrangler/src/__tests__/helpers/mock-oauth-flow.ts

+45
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,56 @@ export const mockOAuthFlow = () => {
105105
return outcome;
106106
};
107107

108+
const mockExchangeRefreshTokenForAccessToken = ({
109+
respondWith,
110+
}: {
111+
respondWith: "refreshSuccess" | "refreshError" | "badResponse";
112+
}) => {
113+
fetchMock.mockOnceIf(
114+
"https://dash.cloudflare.com/oauth2/token",
115+
async () => {
116+
switch (respondWith) {
117+
case "refreshSuccess":
118+
return {
119+
status: 200,
120+
body: JSON.stringify({
121+
access_token: "access_token_success_mock",
122+
expires_in: 1701,
123+
refresh_token: "refresh_token_sucess_mock",
124+
scope: "scope_success_mock",
125+
token_type: "bearer",
126+
}),
127+
};
128+
case "refreshError":
129+
return {
130+
status: 400,
131+
body: JSON.stringify({
132+
error: "invalid_request",
133+
error_description: "error_description_mock",
134+
error_hint: "error_hint_mock",
135+
error_verbose: "error_verbose_mock",
136+
status_code: 400,
137+
}),
138+
};
139+
case "badResponse":
140+
return {
141+
status: 400,
142+
body: `<html> <body> This shouldn't be sent, but should be handled </body> </html>`,
143+
};
144+
145+
default:
146+
return "Not a respondWith option for `mockExchangeRefreshTokenForAccessToken`";
147+
}
148+
}
149+
);
150+
};
151+
108152
return {
109153
mockOAuthServerCallback,
110154
mockGrantAuthorization,
111155
mockRevokeAuthorization,
112156
mockGrantAccessToken,
157+
mockExchangeRefreshTokenForAccessToken,
113158
};
114159
};
115160

packages/wrangler/src/__tests__/user.test.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,26 @@ import path from "node:path";
44
import fetchMock from "jest-fetch-mock";
55
import {
66
readAuthConfigFile,
7+
requireAuth,
78
USER_AUTH_CONFIG_FILE,
89
writeAuthConfigFile,
910
} from "../user";
1011
import { mockConsoleMethods } from "./helpers/mock-console";
1112
import { mockOAuthFlow } from "./helpers/mock-oauth-flow";
1213
import { runInTempDir } from "./helpers/run-in-tmp";
1314
import { runWrangler } from "./helpers/run-wrangler";
15+
import type { Config } from "../config";
1416
import type { UserAuthConfig } from "../user";
1517

16-
describe("wrangler", () => {
18+
describe("User", () => {
1719
runInTempDir({ homedir: "./home" });
1820
const std = mockConsoleMethods();
1921
const {
2022
mockOAuthServerCallback,
2123
mockGrantAccessToken,
2224
mockGrantAuthorization,
2325
mockRevokeAuthorization,
26+
mockExchangeRefreshTokenForAccessToken,
2427
} = mockOAuthFlow();
2528

2629
describe("login", () => {
@@ -82,4 +85,41 @@ describe("wrangler", () => {
8285
expect(fs.existsSync(config)).toBeFalsy();
8386
});
8487
});
88+
89+
// TODO: Improve OAuth mocking to handle `/token` endpoints from different calls
90+
it("should report error message for failed token refresh", async () => {
91+
mockOAuthServerCallback();
92+
writeAuthConfigFile({
93+
oauth_token: "hunter2",
94+
refresh_token: "Order 66",
95+
});
96+
mockGrantAuthorization({ respondWith: "success" });
97+
98+
mockExchangeRefreshTokenForAccessToken({
99+
respondWith: "badResponse",
100+
});
101+
102+
// Handles the requireAuth error throw from failed login that is unhandled due to directly calling it here
103+
await expect(requireAuth({} as Config, false)).rejects.toThrowError();
104+
expect(std.err).toContain(
105+
`Error: <html> <body> This shouldn't be sent, but should be handled </body> </html>`
106+
);
107+
});
108+
109+
it("should confirm no error message when refresh is successful", async () => {
110+
mockOAuthServerCallback();
111+
writeAuthConfigFile({
112+
oauth_token: "hunter2",
113+
refresh_token: "Order 66",
114+
});
115+
mockGrantAuthorization({ respondWith: "success" });
116+
117+
mockExchangeRefreshTokenForAccessToken({
118+
respondWith: "refreshSuccess",
119+
});
120+
121+
// Handles the requireAuth error throw from failed login that is unhandled due to directly calling it here
122+
await expect(requireAuth({} as Config, false)).rejects.toThrowError();
123+
expect(std.err).toContain("");
124+
});
85125
});

packages/wrangler/src/user.tsx

+20-1
Original file line numberDiff line numberDiff line change
@@ -661,8 +661,27 @@ async function exchangeRefreshTokenForAccessToken(): Promise<AccessContext> {
661661
"Content-Type": "application/x-www-form-urlencoded",
662662
},
663663
});
664+
664665
if (response.status >= 400) {
665-
throw await response.json();
666+
let tokenExchangeResErr = undefined;
667+
668+
try {
669+
tokenExchangeResErr = await response.text();
670+
tokenExchangeResErr = JSON.parse(tokenExchangeResErr);
671+
} catch (e) {
672+
// If it can't parse to JSON ignore the error
673+
}
674+
675+
if (tokenExchangeResErr !== undefined) {
676+
// We will throw the parsed error if it parsed correctly, otherwise we throw an unknown error.
677+
throw typeof tokenExchangeResErr === "string"
678+
? new Error(tokenExchangeResErr)
679+
: tokenExchangeResErr;
680+
} else {
681+
throw new ErrorUnknown(
682+
"Failed to parse Error from exchangeRefreshTokenForAccessToken"
683+
);
684+
}
666685
} else {
667686
try {
668687
const json = (await response.json()) as TokenResponse;

0 commit comments

Comments
 (0)