Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved HTTP errors for REST API #444

Merged
merged 9 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions e2e_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const handler = await createHandler(manifest, options);

function assertResponseNotFound(resp: Response) {
assertFalse(resp.ok);
assertEquals(resp.body, null);
assertEquals(resp.status, Status.NotFound);
}

Expand Down Expand Up @@ -204,7 +203,9 @@ Deno.test("[e2e]", async (test) => {
const req = new Request("http://localhost/api/items/" + item.id);

const resp1 = await handler(req);
assertResponseNotFound(resp1);
assertFalse(resp1.ok);
assertEquals(await resp1.text(), "Item not found");
assertEquals(resp1.status, Status.NotFound);

await createItem(item);
const resp2 = await handler(req);
Expand All @@ -221,7 +222,9 @@ Deno.test("[e2e]", async (test) => {
const req = new Request(`http://localhost/api/items/${item.id}/comments`);

const resp1 = await handler(req);
assertResponseNotFound(resp1);
assertFalse(resp1.ok);
assertEquals(await resp1.text(), "Item not found");
assertEquals(resp1.status, Status.NotFound);

await createItem(item);
await createComment(comment);
Expand Down Expand Up @@ -250,7 +253,9 @@ Deno.test("[e2e]", async (test) => {
const req = new Request("http://localhost/api/users/" + user.login);

const resp1 = await handler(req);
assertResponseNotFound(resp1);
assertFalse(resp1.ok);
assertEquals(await resp1.text(), "User not found");
assertEquals(resp1.status, Status.NotFound);

await createUser(user);
const resp2 = await handler(req);
Expand Down Expand Up @@ -288,7 +293,6 @@ Deno.test("[e2e]", async (test) => {

const resp1 = await handler(new Request(url));
assertFalse(resp1.ok);
assertEquals(resp1.body, null);
assertEquals(resp1.status, Status.Unauthorized);

await createUser(user);
Expand Down
106 changes: 54 additions & 52 deletions fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,33 @@ import * as $4 from "./routes/account/_middleware.ts";
import * as $5 from "./routes/account/index.tsx";
import * as $6 from "./routes/account/manage.ts";
import * as $7 from "./routes/account/upgrade.ts";
import * as $8 from "./routes/api/items/[id]/comments.ts";
import * as $9 from "./routes/api/items/[id]/index.ts";
import * as $10 from "./routes/api/items/[id]/vote.ts";
import * as $11 from "./routes/api/items/index.ts";
import * as $12 from "./routes/api/me/notifications.ts";
import * as $13 from "./routes/api/me/votes.ts";
import * as $14 from "./routes/api/stripe-webhooks.ts";
import * as $15 from "./routes/api/users/[login]/index.ts";
import * as $16 from "./routes/api/users/[login]/items.ts";
import * as $17 from "./routes/api/users/index.ts";
import * as $18 from "./routes/blog/[slug].tsx";
import * as $19 from "./routes/blog/index.tsx";
import * as $20 from "./routes/dashboard/_middleware.ts";
import * as $21 from "./routes/dashboard/index.tsx";
import * as $22 from "./routes/dashboard/stats.tsx";
import * as $23 from "./routes/dashboard/users.tsx";
import * as $24 from "./routes/feed.ts";
import * as $25 from "./routes/index.tsx";
import * as $26 from "./routes/items/[id].tsx";
import * as $27 from "./routes/notifications/[id].ts";
import * as $28 from "./routes/notifications/_middleware.ts";
import * as $29 from "./routes/notifications/index.tsx";
import * as $30 from "./routes/pricing.tsx";
import * as $31 from "./routes/submit/_middleware.tsx";
import * as $32 from "./routes/submit/index.tsx";
import * as $33 from "./routes/users/[login].tsx";
import * as $8 from "./routes/api/_middleware.ts";
import * as $9 from "./routes/api/items/[id]/comments.ts";
import * as $10 from "./routes/api/items/[id]/index.ts";
import * as $11 from "./routes/api/items/[id]/vote.ts";
import * as $12 from "./routes/api/items/index.ts";
import * as $13 from "./routes/api/me/notifications.ts";
import * as $14 from "./routes/api/me/votes.ts";
import * as $15 from "./routes/api/stripe-webhooks.ts";
import * as $16 from "./routes/api/users/[login]/index.ts";
import * as $17 from "./routes/api/users/[login]/items.ts";
import * as $18 from "./routes/api/users/index.ts";
import * as $19 from "./routes/blog/[slug].tsx";
import * as $20 from "./routes/blog/index.tsx";
import * as $21 from "./routes/dashboard/_middleware.ts";
import * as $22 from "./routes/dashboard/index.tsx";
import * as $23 from "./routes/dashboard/stats.tsx";
import * as $24 from "./routes/dashboard/users.tsx";
import * as $25 from "./routes/feed.ts";
import * as $26 from "./routes/index.tsx";
import * as $27 from "./routes/items/[id].tsx";
import * as $28 from "./routes/notifications/[id].ts";
import * as $29 from "./routes/notifications/_middleware.ts";
import * as $30 from "./routes/notifications/index.tsx";
import * as $31 from "./routes/pricing.tsx";
import * as $32 from "./routes/submit/_middleware.tsx";
import * as $33 from "./routes/submit/index.tsx";
import * as $34 from "./routes/users/[login].tsx";
import * as $$0 from "./islands/Chart.tsx";
import * as $$1 from "./islands/CommentsList.tsx";
import * as $$2 from "./islands/ItemsList.tsx";
Expand All @@ -53,32 +54,33 @@ const manifest = {
"./routes/account/index.tsx": $5,
"./routes/account/manage.ts": $6,
"./routes/account/upgrade.ts": $7,
"./routes/api/items/[id]/comments.ts": $8,
"./routes/api/items/[id]/index.ts": $9,
"./routes/api/items/[id]/vote.ts": $10,
"./routes/api/items/index.ts": $11,
"./routes/api/me/notifications.ts": $12,
"./routes/api/me/votes.ts": $13,
"./routes/api/stripe-webhooks.ts": $14,
"./routes/api/users/[login]/index.ts": $15,
"./routes/api/users/[login]/items.ts": $16,
"./routes/api/users/index.ts": $17,
"./routes/blog/[slug].tsx": $18,
"./routes/blog/index.tsx": $19,
"./routes/dashboard/_middleware.ts": $20,
"./routes/dashboard/index.tsx": $21,
"./routes/dashboard/stats.tsx": $22,
"./routes/dashboard/users.tsx": $23,
"./routes/feed.ts": $24,
"./routes/index.tsx": $25,
"./routes/items/[id].tsx": $26,
"./routes/notifications/[id].ts": $27,
"./routes/notifications/_middleware.ts": $28,
"./routes/notifications/index.tsx": $29,
"./routes/pricing.tsx": $30,
"./routes/submit/_middleware.tsx": $31,
"./routes/submit/index.tsx": $32,
"./routes/users/[login].tsx": $33,
"./routes/api/_middleware.ts": $8,
"./routes/api/items/[id]/comments.ts": $9,
"./routes/api/items/[id]/index.ts": $10,
"./routes/api/items/[id]/vote.ts": $11,
"./routes/api/items/index.ts": $12,
"./routes/api/me/notifications.ts": $13,
"./routes/api/me/votes.ts": $14,
"./routes/api/stripe-webhooks.ts": $15,
"./routes/api/users/[login]/index.ts": $16,
"./routes/api/users/[login]/items.ts": $17,
"./routes/api/users/index.ts": $18,
"./routes/blog/[slug].tsx": $19,
"./routes/blog/index.tsx": $20,
"./routes/dashboard/_middleware.ts": $21,
"./routes/dashboard/index.tsx": $22,
"./routes/dashboard/stats.tsx": $23,
"./routes/dashboard/users.tsx": $24,
"./routes/feed.ts": $25,
"./routes/index.tsx": $26,
"./routes/items/[id].tsx": $27,
"./routes/notifications/[id].ts": $28,
"./routes/notifications/_middleware.ts": $29,
"./routes/notifications/index.tsx": $30,
"./routes/pricing.tsx": $31,
"./routes/submit/_middleware.tsx": $32,
"./routes/submit/index.tsx": $33,
"./routes/users/[login].tsx": $34,
},
islands: {
"./islands/Chart.tsx": $$0,
Expand Down
20 changes: 20 additions & 0 deletions routes/api/_middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.

import { type MiddlewareHandlerContext, Status } from "$fresh/server.ts";
import { isHttpError } from "std/http/http_errors.ts";

export async function handler(
_req: Request,
ctx: MiddlewareHandlerContext,
) {
try {
return await ctx.next();
} catch (error) {
return isHttpError(error)
? new Response(error.message, {
status: error.status,
headers: error.headers,
})
: new Response(error.message, { status: Status.InternalServerError });
}
}
5 changes: 3 additions & 2 deletions routes/api/items/[id]/comments.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Handlers, Status } from "$fresh/server.ts";
import type { Handlers } from "$fresh/server.ts";
import { collectValues, getItem, listCommentsByItem } from "@/utils/db.ts";
import { errors } from "std/http/http_errors.ts";
import { getCursor } from "@/utils/http.ts";

// Copyright 2023 the Deno authors. All rights reserved. MIT license.
export const handler: Handlers = {
async GET(req, ctx) {
const itemId = ctx.params.id;
const item = await getItem(itemId);
if (item === null) return new Response(null, { status: Status.NotFound });
if (item === null) throw new errors.NotFound("Item not found");

const url = new URL(req.url);
const iter = listCommentsByItem(itemId, {
Expand Down
8 changes: 4 additions & 4 deletions routes/api/items/[id]/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// 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 { getItem } from "@/utils/db.ts";
import { errors } from "std/http/http_errors.ts";

export const handler: Handlers = {
async GET(_req, ctx) {
const item = await getItem(ctx.params.id);
return item === null
? new Response(null, { status: Status.NotFound })
: Response.json(item);
if (item === null) throw new errors.NotFound("Item not found");
return Response.json(item);
},
};
13 changes: 7 additions & 6 deletions routes/api/items/[id]/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ import {
newNotificationProps,
newVoteProps,
} from "@/utils/db.ts";
import { errors } from "std/http/http_errors.ts";

export const handler: Handlers<undefined, State> = {
async POST(_req, ctx) {
const itemId = ctx.params.id;
const item = await getItem(itemId);
if (item === null) return new Response(null, { status: Status.NotFound });
if (item === null) throw new errors.NotFound("Item not found");

if (ctx.state.sessionId === undefined) {
return new Response(null, { status: Status.Unauthorized });
throw new errors.Unauthorized("User must be signed in");
}
const user = await getUserBySession(ctx.state.sessionId);
if (user === null) return new Response(null, { status: Status.NotFound });
if (user === null) throw new errors.NotFound("User not found");

await createVote({
itemId,
Expand All @@ -44,13 +45,13 @@ export const handler: Handlers<undefined, State> = {
async DELETE(_req, ctx) {
const itemId = ctx.params.id;
const item = await getItem(itemId);
if (item === null) return new Response(null, { status: Status.NotFound });
if (item === null) throw new errors.NotFound("Item not found");

if (ctx.state.sessionId === undefined) {
return new Response(null, { status: Status.Unauthorized });
throw new errors.Unauthorized("User must be signed in");
}
const user = await getUserBySession(ctx.state.sessionId);
if (user === null) return new Response(null, { status: Status.NotFound });
if (user === null) throw new errors.NotFound("User not found");

await deleteVote({ itemId, userLogin: user.login });

Expand Down
7 changes: 4 additions & 3 deletions routes/api/me/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
// 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 {
collectValues,
getUserBySession,
listNotificationsByUser,
} from "@/utils/db.ts";
import { getCursor } from "@/utils/http.ts";
import { State } from "@/routes/_middleware.ts";
import { errors } from "std/http/http_errors.ts";

export const handler: Handlers<undefined, State> = {
async GET(req, ctx) {
if (ctx.state.sessionId === undefined) {
return new Response(null, { status: Status.Unauthorized });
throw new errors.Unauthorized("User must be signed in");
}

const user = await getUserBySession(ctx.state.sessionId);
if (user === null) return new Response(null, { status: Status.NotFound });
if (user === null) throw new errors.NotFound("User not found");

const url = new URL(req.url);
const iter = listNotificationsByUser(user.login, {
Expand Down
7 changes: 4 additions & 3 deletions routes/api/me/votes.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
// 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 type { State } from "@/routes/_middleware.ts";
import {
collectValues,
getUserBySession,
listItemsVotedByUser,
} from "@/utils/db.ts";
import { errors } from "std/http/http_errors.ts";

export const handler: Handlers<undefined, State> = {
async GET(_req, ctx) {
if (ctx.state.sessionId === undefined) {
return new Response(null, { status: Status.Unauthorized });
throw new errors.Unauthorized("User must be signed in");
}

const user = await getUserBySession(ctx.state.sessionId);
if (user === null) return new Response(null, { status: Status.NotFound });
if (user === null) throw new errors.NotFound("User not found");

const iter = listItemsVotedByUser(user.login);
const items = await collectValues(iter);
Expand Down
19 changes: 9 additions & 10 deletions routes/api/stripe-webhooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import type { Handlers } from "$fresh/server.ts";
import { type Handlers, Status } from "$fresh/server.ts";
import { stripe } from "@/utils/payments.ts";
import Stripe from "stripe";
import { getUserByStripeCustomer, updateUser } from "@/utils/db.ts";
import { errors } from "std/http/http_errors.ts";

const cryptoProvider = Stripe.createSubtleCryptoProvider();

Expand All @@ -12,8 +13,8 @@ export const handler: Handlers = {
* 1. customer.subscription.created (when a user subscribes to the premium plan)
* 2. customer.subscription.deleted (when a user cancels the premium plan)
*/
async POST(req, ctx) {
if (stripe === undefined) return await ctx.renderNotFound();
async POST(req) {
if (stripe === undefined) throw new errors.NotFound();

const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
Expand All @@ -39,20 +40,18 @@ export const handler: Handlers = {
switch (event.type) {
case "customer.subscription.created": {
const user = await getUserByStripeCustomer(customer);
if (!user) return new Response(null, { status: 400 });
if (user === null) throw new errors.NotFound("User not found");
await updateUser({ ...user, isSubscribed: true });
return new Response(null, { status: 201 });
return new Response(null, { status: Status.Created });
}
case "customer.subscription.deleted": {
const user = await getUserByStripeCustomer(customer);
if (!user) return new Response(null, { status: 400 });
if (user === null) throw new errors.NotFound("User not found");
await updateUser({ ...user, isSubscribed: false });
return new Response(null, { status: 202 });
return new Response(null, { status: Status.Accepted });
}
default: {
const message = `Event type not supported: ${event.type}`;
console.error(message);
return new Response(message, { status: 400 });
throw new errors.BadRequest("Event type not supported");
}
}
},
Expand Down
8 changes: 4 additions & 4 deletions routes/api/users/[login]/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { Handlers, Status } from "$fresh/server.ts";
import type { Handlers } from "$fresh/server.ts";
import { getUser } from "@/utils/db.ts";
import { errors } from "std/http/http_errors.ts";

export const handler: Handlers = {
async GET(_req, ctx) {
const user = await getUser(ctx.params.login);
return user === null
? new Response(null, { status: Status.NotFound })
: Response.json(user);
if (user === null) throw new errors.NotFound("User not found");
return Response.json(user);
},
};
5 changes: 3 additions & 2 deletions routes/api/users/[login]/items.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.
import { Handlers, Status } from "$fresh/server.ts";
import type { Handlers } from "$fresh/server.ts";
import { collectValues, getUser, listItemsByUser } from "@/utils/db.ts";
import { getCursor } from "@/utils/http.ts";
import { errors } from "std/http/http_errors.ts";

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

const url = new URL(req.url);
const iter = listItemsByUser(ctx.params.login, {
Expand Down