Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit63807e5unknown_key

fix(api): degrade to 503 instead of 500 on DB failure

fix(api): degrade to 503 instead of 500 on DB failure

The REST API was throwing 500s when the DB was unreachable (e.g. during
tests without DATABASE_URL, or during a Neon outage). Violates invariant
"every user-facing failure mode has a fallback — no 500s reach the UI."

Wraps /api/repos (GET + POST), /api/users/:u/repos, /api/setup with
try/catch → 503 Service Unavailable + logged error. Validation errors
still return 400. Missing records still return 404.

Tests updated: the old [404, 500] allowance is replaced with a strict
[404, 503] — 500 is no longer acceptable. Added coverage for
/api/users/:u/repos which had none.

https://claude.ai/code/session_01U6sj9ezKCpmiwx7cXGZmS6
Claude committed on April 14, 2026Parent: 5cc5d95
2 files changed+15410963807e5da3e3b49a0d1e3f4afae52880dc4500fb
2 changed files+154−109
Modifiedsrc/__tests__/api.test.ts+12−7View fileUnifiedSplit
2323 expect(text).toContain("gluecron");
2424 });
2525
26 it("GET /api/repos/:owner/:name returns 404 for missing repo", async () => {
27 // This will fail without DB, but verifies route exists
26 it("GET /api/repos/:owner/:name returns 404 or 503, never 500", async () => {
2827 const res = await app.request("/api/repos/nobody/nothing");
29 // Without DB connection, this returns 500 or 404
30 expect([404, 500]).toContain(res.status);
28 // Without DB: 503 (db unreachable). With DB + missing repo: 404.
29 // API must degrade gracefully — 500 is NOT acceptable.
30 expect([404, 503]).toContain(res.status);
3131 });
3232
33 it("POST /api/repos returns 400 without required fields", async () => {
33 it("POST /api/repos returns 400 without required fields, never 500", async () => {
3434 const res = await app.request("/api/repos", {
3535 method: "POST",
3636 headers: { "Content-Type": "application/json" },
3737 body: JSON.stringify({}),
3838 });
39 // Without DB, might be 400 or 500
40 expect([400, 500]).toContain(res.status);
39 // Validation happens before DB access — always 400.
40 expect(res.status).toBe(400);
41 });
42
43 it("GET /api/users/:u/repos returns 404 or 503, never 500", async () => {
44 const res = await app.request("/api/users/nobody/repos");
45 expect([404, 503]).toContain(res.status);
4146 });
4247});
4348
Modifiedsrc/routes/api.ts+142−102View fileUnifiedSplit
1313
1414// Create repository
1515api.post("/repos", async (c) => {
16 const body = await c.req.json<{
16 let body: {
1717 name: string;
1818 owner: string;
1919 description?: string;
2020 isPrivate?: boolean;
21 }>();
21 };
22 try {
23 body = await c.req.json();
24 } catch {
25 return c.json({ error: "Invalid JSON body" }, 400);
26 }
2227
2328 if (!body.name || !body.owner) {
2429 return c.json({ error: "name and owner are required" }, 400);
2934 return c.json({ error: "Invalid repository name" }, 400);
3035 }
3136
32 // Find owner
33 const [owner] = await db
34 .select()
35 .from(users)
36 .where(eq(users.username, body.owner));
37 try {
38 // Find owner
39 const [owner] = await db
40 .select()
41 .from(users)
42 .where(eq(users.username, body.owner));
3743
38 if (!owner) {
39 return c.json({ error: "Owner not found" }, 404);
40 }
44 if (!owner) {
45 return c.json({ error: "Owner not found" }, 404);
46 }
4147
42 // Check duplicate
43 if (await repoExists(body.owner, body.name)) {
44 return c.json({ error: "Repository already exists" }, 409);
45 }
48 // Check duplicate
49 if (await repoExists(body.owner, body.name)) {
50 return c.json({ error: "Repository already exists" }, 409);
51 }
4652
47 // Init bare repo on disk
48 const diskPath = await initBareRepo(body.owner, body.name);
49
50 // Insert into DB
51 const [repo] = await db
52 .insert(repositories)
53 .values({
54 name: body.name,
55 ownerId: owner.id,
56 description: body.description || null,
57 isPrivate: body.isPrivate || false,
58 diskPath,
59 })
60 .returning();
61
62 // Green-ecosystem bootstrap: settings, protection, labels, welcome issue
63 if (repo) {
64 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
65 await bootstrapRepository({
66 repositoryId: repo.id,
67 ownerUserId: owner.id,
68 });
69 }
53 // Init bare repo on disk
54 const diskPath = await initBareRepo(body.owner, body.name);
55
56 // Insert into DB
57 const [repo] = await db
58 .insert(repositories)
59 .values({
60 name: body.name,
61 ownerId: owner.id,
62 description: body.description || null,
63 isPrivate: body.isPrivate || false,
64 diskPath,
65 })
66 .returning();
7067
71 return c.json(repo, 201);
68 // Green-ecosystem bootstrap: settings, protection, labels, welcome issue
69 if (repo) {
70 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
71 await bootstrapRepository({
72 repositoryId: repo.id,
73 ownerUserId: owner.id,
74 });
75 }
76
77 return c.json(repo, 201);
78 } catch (err) {
79 console.error("[api] POST /repos:", err);
80 return c.json({ error: "Service unavailable" }, 503);
81 }
7282});
7383
7484// List user's repositories
7585api.get("/users/:username/repos", async (c) => {
7686 const { username } = c.req.param();
77 const [owner] = await db
78 .select()
79 .from(users)
80 .where(eq(users.username, username));
81 if (!owner) return c.json({ error: "User not found" }, 404);
82
83 const repos = await db
84 .select()
85 .from(repositories)
86 .where(eq(repositories.ownerId, owner.id));
87
88 return c.json(repos);
87 try {
88 const [owner] = await db
89 .select()
90 .from(users)
91 .where(eq(users.username, username));
92 if (!owner) return c.json({ error: "User not found" }, 404);
93
94 const repos = await db
95 .select()
96 .from(repositories)
97 .where(eq(repositories.ownerId, owner.id));
98
99 return c.json(repos);
100 } catch (err) {
101 console.error("[api] /users/:username/repos:", err);
102 return c.json({ error: "Service unavailable" }, 503);
103 }
89104});
90105
91106// Get single repository
92107api.get("/repos/:owner/:name", async (c) => {
93108 const { owner: ownerName, name } = c.req.param();
94 const [owner] = await db
95 .select()
96 .from(users)
97 .where(eq(users.username, ownerName));
98 if (!owner) return c.json({ error: "Not found" }, 404);
99
100 const [repo] = await db
101 .select()
102 .from(repositories)
103 .where(
104 and(eq(repositories.ownerId, owner.id), eq(repositories.name, name))
105 );
106 if (!repo) return c.json({ error: "Not found" }, 404);
109 try {
110 const [owner] = await db
111 .select()
112 .from(users)
113 .where(eq(users.username, ownerName));
114 if (!owner) return c.json({ error: "Not found" }, 404);
107115
108 return c.json(repo);
116 const [repo] = await db
117 .select()
118 .from(repositories)
119 .where(
120 and(eq(repositories.ownerId, owner.id), eq(repositories.name, name))
121 );
122 if (!repo) return c.json({ error: "Not found" }, 404);
123
124 return c.json(repo);
125 } catch (err) {
126 console.error("[api] /repos/:owner/:name:", err);
127 return c.json({ error: "Service unavailable" }, 503);
128 }
109129});
110130
111131// Quick-setup: create user + repo in one call (dev convenience)
112132api.post("/setup", async (c) => {
113 const body = await c.req.json<{
133 let body: {
114134 username: string;
115135 email: string;
116136 repoName: string;
117137 description?: string;
118 }>();
119
120 // Upsert user
121 let [user] = await db
122 .select()
123 .from(users)
124 .where(eq(users.username, body.username));
125
126 if (!user) {
127 [user] = await db
128 .insert(users)
129 .values({
130 username: body.username,
131 email: body.email,
132 passwordHash: await hashPassword("changeme"),
133 })
134 .returning();
138 };
139 try {
140 body = await c.req.json();
141 } catch {
142 return c.json({ error: "Invalid JSON body" }, 400);
143 }
144 if (!body.username || !body.email || !body.repoName) {
145 return c.json(
146 { error: "username, email, and repoName are required" },
147 400
148 );
135149 }
136150
137 // Create repo if not exists
138 if (!(await repoExists(body.username, body.repoName))) {
139 const diskPath = await initBareRepo(body.username, body.repoName);
140 const [repo] = await db
141 .insert(repositories)
142 .values({
143 name: body.repoName,
144 ownerId: user.id,
145 description: body.description || null,
146 diskPath,
147 })
148 .returning();
149 if (repo) {
150 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
151 await bootstrapRepository({
152 repositoryId: repo.id,
153 ownerUserId: user.id,
154 });
151 try {
152 // Upsert user
153 let [user] = await db
154 .select()
155 .from(users)
156 .where(eq(users.username, body.username));
157
158 if (!user) {
159 [user] = await db
160 .insert(users)
161 .values({
162 username: body.username,
163 email: body.email,
164 passwordHash: await hashPassword("changeme"),
165 })
166 .returning();
167 }
168
169 // Create repo if not exists
170 if (!(await repoExists(body.username, body.repoName))) {
171 const diskPath = await initBareRepo(body.username, body.repoName);
172 const [repo] = await db
173 .insert(repositories)
174 .values({
175 name: body.repoName,
176 ownerId: user.id,
177 description: body.description || null,
178 diskPath,
179 })
180 .returning();
181 if (repo) {
182 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
183 await bootstrapRepository({
184 repositoryId: repo.id,
185 ownerUserId: user.id,
186 });
187 }
155188 }
156 }
157189
158 return c.json({ user: user.username, repo: body.repoName, status: "ready" });
190 return c.json({
191 user: user.username,
192 repo: body.repoName,
193 status: "ready",
194 });
195 } catch (err) {
196 console.error("[api] POST /setup:", err);
197 return c.json({ error: "Service unavailable" }, 503);
198 }
159199});
160200
161201export default api;
162202