Skip to content

Commit

Permalink
refactor: remove use of std/http/http_errors (#638)
Browse files Browse the repository at this point in the history
  • Loading branch information
iuioiua authored Oct 31, 2023
1 parent b9a4b4e commit c7062b2
Show file tree
Hide file tree
Showing 11 changed files with 52 additions and 65 deletions.
4 changes: 2 additions & 2 deletions fresh.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import sessionPlugin from "./plugins/session.ts";
import errorHandling from "./plugins/error_handling.ts";
import securityHeaders from "./plugins/security_headers.ts";
import welcomePlugin from "./plugins/welcome.ts";
import { FreshOptions } from "$fresh/server.ts";
import type { FreshConfig } from "$fresh/server.ts";

export default {
plugins: [
Expand All @@ -17,4 +17,4 @@ export default {
errorHandling,
securityHeaders,
],
} as FreshOptions;
} as FreshConfig;
49 changes: 17 additions & 32 deletions plugins/error_handling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,25 @@
import type { Plugin } from "$fresh/server.ts";
import type { State } from "@/plugins/session.ts";
import { Status } from "$fresh/server.ts";
import { errors, isHttpError } from "std/http/http_errors.ts";
import { redirect } from "@/utils/http.ts";
import { BadRequestError, redirect, UnauthorizedError } from "@/utils/http.ts";
import { STATUS_TEXT } from "std/http/http_status.ts";

/**
* Returns the converted HTTP error response from the given error. If the error
* is an instance of {@linkcode Deno.errors.NotFound}, a HTTP 404 Not Found
* error response is returned. This is done to translate errors thrown from
* logic that's separated by concerns.
*
* If the error is a HTTP-flavored error, the corresponding HTTP error response
* is returned.
*
* If the error is a generic error, a HTTP 500 Internal Server error response
* is returned.
*
* @see {@link https://deno.land/std/http/http_errors.ts}
* Returns the HTTP status code corresponding to a given runtime error. By
* default, a HTTP 500 status code is returned.
*
* @example
* ```ts
* import { toErrorResponse } from "@/plugins/error_handling.ts";
* import { errors } from "std/http/http_errors.ts";
* import { toErrorStatus } from "@/plugins/error_handling.ts";
*
* const resp = toErrorResponse(new errors.NotFound("User not found"));
* resp.status; // Returns 404
* await resp.text(); // Returns "User not found"
* toErrorStatus(new Deno.errors.NotFound) // Returns 404
* ```
*/
// deno-lint-ignore no-explicit-any
export function toErrorResponse(error: any) {
if (error instanceof Deno.errors.NotFound) {
return new Response(error.message, { status: Status.NotFound });
}
return isHttpError(error)
? new Response(error.message, {
status: error.status,
headers: error.headers,
})
: new Response(error.message, { status: Status.InternalServerError });
export function toErrorStatus(error: Error) {
if (error instanceof Deno.errors.NotFound) return Status.NotFound;
if (error instanceof UnauthorizedError) return Status.Unauthorized;
if (error instanceof BadRequestError) return Status.BadRequest;
return Status.InternalServerError;
}

export default {
Expand All @@ -52,7 +33,7 @@ export default {
try {
return await ctx.next();
} catch (error) {
if (error instanceof errors.Unauthorized) {
if (error instanceof UnauthorizedError) {
return redirect("/signin");
}
throw error;
Expand All @@ -67,7 +48,11 @@ export default {
try {
return await ctx.next();
} catch (error) {
return toErrorResponse(error);
const status = toErrorStatus(error);
return new Response(error.message, {
statusText: STATUS_TEXT[status],
status,
});
}
},
},
Expand Down
5 changes: 2 additions & 3 deletions plugins/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import type { MiddlewareHandlerContext } from "$fresh/server.ts";
import { getSessionId } from "kv_oauth/mod.ts";
import { getUserBySession } from "@/utils/db.ts";
import type { User } from "@/utils/db.ts";
import { createHttpError } from "std/http/http_errors.ts";
import { Status } from "std/http/http_status.ts";
import { UnauthorizedError } from "@/utils/http.ts";

export interface State {
sessionUser?: User;
Expand All @@ -17,7 +16,7 @@ export function assertSignedIn(
ctx: { state: State },
): asserts ctx is { state: SignedInState } {
if (ctx.state.sessionUser === undefined) {
throw createHttpError(Status.Unauthorized, "User must be signed in");
throw new UnauthorizedError("User must be signed in");
}
}

Expand Down
4 changes: 1 addition & 3 deletions routes/api/items/[id].ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import type { Handlers } from "$fresh/server.ts";
import { getItem } from "@/utils/db.ts";
import { Status } from "std/http/http_status.ts";
import { createHttpError } from "std/http/http_errors.ts";

export const handler: Handlers = {
async GET(_req, ctx) {
const item = await getItem(ctx.params.id);
if (item === null) throw createHttpError(Status.NotFound, "Item not found");
if (item === null) throw new Deno.errors.NotFound("Item not found");
return Response.json(item);
},
};
17 changes: 7 additions & 10 deletions routes/api/stripe-webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type Handlers, Status } from "$fresh/server.ts";
import { isStripeEnabled, stripe } from "@/utils/stripe.ts";
import Stripe from "stripe";
import { getUserByStripeCustomer, updateUser } from "@/utils/db.ts";
import { createHttpError } from "std/http/http_errors.ts";
import { BadRequestError } from "@/utils/http.ts";

const cryptoProvider = Stripe.createSubtleCryptoProvider();
export const handler: Handlers = {
Expand All @@ -15,16 +15,13 @@ export const handler: Handlers = {
* @see {@link https://github.com/stripe-samples/stripe-node-deno-samples/blob/2d571b20cd88f1c1f02185483729a37210484c68/webhook-signing/main.js}
*/
async POST(req) {
if (!isStripeEnabled()) throw createHttpError(Status.NotFound);
if (!isStripeEnabled()) throw new Deno.errors.NotFound("Not Found");

/** @see {@link https://stripe.com/docs/webhooks#verify-events} */
const body = await req.text();
const signature = req.headers.get("stripe-signature");
if (signature === null) {
throw createHttpError(
Status.BadRequest,
"`Stripe-Signature` header is missing",
);
throw new BadRequestError("`Stripe-Signature` header is missing");
}
const signingSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET");
if (signingSecret === undefined) {
Expand All @@ -43,7 +40,7 @@ export const handler: Handlers = {
cryptoProvider,
);
} catch (error) {
throw createHttpError(Status.BadRequest, error.message);
throw new BadRequestError(error.message);
}

// @ts-ignore: Property 'customer' actually does exist on type 'Object'
Expand All @@ -53,7 +50,7 @@ export const handler: Handlers = {
case "customer.subscription.created": {
const user = await getUserByStripeCustomer(customer);
if (user === null) {
throw createHttpError(Status.NotFound, "User not found");
throw new Deno.errors.NotFound("User not found");
}

await updateUser({ ...user, isSubscribed: true });
Expand All @@ -62,14 +59,14 @@ export const handler: Handlers = {
case "customer.subscription.deleted": {
const user = await getUserByStripeCustomer(customer);
if (user === null) {
throw createHttpError(Status.NotFound, "User not found");
throw new Deno.errors.NotFound("User not found");
}

await updateUser({ ...user, isSubscribed: false });
return new Response(null, { status: Status.Accepted });
}
default: {
throw createHttpError(Status.BadRequest, "Event type not supported");
throw new BadRequestError("Event type not supported");
}
}
},
Expand Down
5 changes: 2 additions & 3 deletions routes/api/users/[login]/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { type Handlers, Status } from "$fresh/server.ts";
import type { Handlers } from "$fresh/server.ts";
import { getUser } from "@/utils/db.ts";
import { createHttpError } from "std/http/http_errors.ts";

export const handler: Handlers = {
async GET(_req, ctx) {
const user = await getUser(ctx.params.login);
if (user === null) throw createHttpError(Status.NotFound, "User not found");
if (user === null) throw new Deno.errors.NotFound("User not found");
return Response.json(user);
},
};
4 changes: 1 addition & 3 deletions routes/api/users/[login]/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
import type { Handlers } from "$fresh/server.ts";
import { collectValues, getUser, listItemsByUser } from "@/utils/db.ts";
import { getCursor } from "@/utils/http.ts";
import { Status } from "std/http/http_status.ts";
import { createHttpError } from "std/http/http_errors.ts";

export const handler: Handlers = {
async GET(req, ctx) {
const user = await getUser(ctx.params.login);
if (user === null) throw createHttpError(Status.NotFound, "User not found");
if (user === null) throw new Deno.errors.NotFound("User not found");

const url = new URL(req.url);
const iter = listItemsByUser(ctx.params.login, {
Expand Down
7 changes: 2 additions & 5 deletions routes/api/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@
import { type Handlers, Status } from "$fresh/server.ts";
import type { SignedInState } from "@/plugins/session.ts";
import { createVote } from "@/utils/db.ts";
import { createHttpError } from "std/http/http_errors.ts";
import { BadRequestError } from "@/utils/http.ts";

export const handler: Handlers<undefined, SignedInState> = {
async POST(req, ctx) {
const itemId = new URL(req.url).searchParams.get("item_id");
if (itemId === null) {
throw createHttpError(
Status.BadRequest,
"`item_id` URL parameter missing",
);
throw new BadRequestError("`item_id` URL parameter missing");
}

await createVote({
Expand Down
4 changes: 2 additions & 2 deletions utils/github.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { createHttpError } from "std/http/http_errors.ts";
import { createGitHubOAuthConfig } from "kv_oauth/mod.ts";
import { BadRequestError } from "@/utils/http.ts";

export function isGitHubSetup() {
try {
Expand Down Expand Up @@ -37,7 +37,7 @@ export async function getGitHubUser(accessToken: string) {
});
if (!resp.ok) {
const { message } = await resp.json();
throw createHttpError(resp.status, message);
throw new BadRequestError(message);
}
return await resp.json() as Promise<GitHubUser>;
}
4 changes: 2 additions & 2 deletions utils/github_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import { assertRejects } from "std/assert/assert_rejects.ts";
import { getGitHubUser } from "./github.ts";
import { returnsNext, stub } from "std/testing/mock.ts";
import { errors } from "std/http/http_errors.ts";
import { assertEquals } from "std/assert/assert_equals.ts";
import { Status } from "kv_oauth/deps.ts";
import { BadRequestError } from "@/utils/http.ts";

Deno.test("[plugins] getGitHubUser()", async (test) => {
await test.step("rejects on error message", async () => {
Expand All @@ -20,7 +20,7 @@ Deno.test("[plugins] getGitHubUser()", async (test) => {
);
await assertRejects(
async () => await getGitHubUser(crypto.randomUUID()),
errors.BadRequest,
BadRequestError,
message,
);
fetchStub.restore();
Expand Down
14 changes: 14 additions & 0 deletions utils/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,17 @@ export async function fetchValues<T>(endpoint: string, cursor: string) {
if (!resp.ok) throw new Error(`Request failed: GET ${url}`);
return await resp.json() as { values: T[]; cursor: string };
}

export class UnauthorizedError extends Error {
constructor(message?: string) {
super(message);
this.name = "UnauthorizedError";
}
}

export class BadRequestError extends Error {
constructor(message?: string) {
super(message);
this.name = "BadRequestError";
}
}

0 comments on commit c7062b2

Please sign in to comment.