-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdebug_exercises.py
More file actions
713 lines (712 loc) · 42.4 KB
/
debug_exercises.py
File metadata and controls
713 lines (712 loc) · 42.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
"""Debug This Response exercises — broken HTTP exchanges for users to diagnose."""
DEBUG_EXERCISES = [
# --- Beginner (10 exercises) ---
{
"id": "200-error-body",
"difficulty": "beginner",
"category": "errors",
"title": "200 with Error Body",
"description": "A client requests a user profile, but the user does not exist. The server sends back this response.",
"request": "GET /api/users/99999 HTTP/1.1\nHost: api.example.com\nAccept: application/json\nAuthorization: Bearer eyJhbG...",
"response": 'HTTP/1.1 200 OK\nContent-Type: application/json\n\n{"error": "User not found"}',
"bugs": [
{
"id": "wrong-status",
"description": "Status code should be 404, not 200",
"explanation": "A 200 OK tells the client the request succeeded. When a resource is not found, the server must return 404 Not Found so clients, caches, and monitoring tools handle the error correctly.",
},
],
"related_codes": ["200", "404"],
},
{
"id": "301-no-location",
"difficulty": "beginner",
"category": "redirects",
"title": "301 Missing Location",
"description": "A page has permanently moved to a new URL. The server sends a redirect response.",
"request": "GET /old-page HTTP/1.1\nHost: www.example.com\nAccept: text/html",
"response": "HTTP/1.1 301 Moved Permanently\nContent-Type: text/html\nContent-Length: 0",
"bugs": [
{
"id": "missing-location",
"description": "Missing required Location header",
"explanation": "A 301 redirect without a Location header is useless -- the client has no idea where to go. Browsers will show an error, and bots will not follow the redirect. Always include Location with 3xx redirects.",
},
],
"related_codes": ["301"],
},
{
"id": "204-with-body",
"difficulty": "beginner",
"category": "crud",
"title": "204 with Body",
"description": "A client deletes a resource. The server confirms the deletion.",
"request": "DELETE /api/posts/42 HTTP/1.1\nHost: api.example.com\nAuthorization: Bearer eyJhbG...",
"response": 'HTTP/1.1 204 No Content\nContent-Type: application/json\nContent-Length: 35\n\n{"message": "Post deleted successfully"}',
"bugs": [
{
"id": "body-on-204",
"description": "204 No Content must not include a body",
"explanation": "RFC 9110 states that a 204 response must not contain a message body. Clients are allowed to ignore any body present. If you want to send a confirmation message, use 200 OK instead.",
},
{
"id": "content-headers-on-204",
"description": "Content-Type and Content-Length should not be present on a 204",
"explanation": "Since 204 means no content, including Content-Type and Content-Length headers is misleading and contradicts the status code semantics.",
},
],
"related_codes": ["204", "200"],
},
{
"id": "401-no-www-auth",
"difficulty": "beginner",
"category": "auth",
"title": "401 Missing WWW-Authenticate",
"description": "An unauthenticated client tries to access a protected API endpoint.",
"request": "GET /api/admin/settings HTTP/1.1\nHost: api.example.com\nAccept: application/json",
"response": 'HTTP/1.1 401 Unauthorized\nContent-Type: application/json\n\n{"error": "Authentication required"}',
"bugs": [
{
"id": "missing-www-authenticate",
"description": "Missing required WWW-Authenticate header",
"explanation": "RFC 9110 requires a 401 response to include a WWW-Authenticate header indicating the authentication scheme(s) the server accepts (e.g. Bearer, Basic). Without it, the client has no way to know how to authenticate.",
},
],
"related_codes": ["401"],
},
{
"id": "201-no-location",
"difficulty": "beginner",
"category": "crud",
"title": "201 Without Location",
"description": "A client creates a new blog post via the API.",
"request": 'POST /api/posts HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\nAuthorization: Bearer eyJhbG...\n\n{"title": "Hello World", "body": "My first post"}',
"response": 'HTTP/1.1 201 Created\nContent-Type: application/json\n\n{"id": 1, "title": "Hello World", "body": "My first post"}',
"bugs": [
{
"id": "missing-location-201",
"description": "Missing Location header pointing to the new resource",
"explanation": "When a server returns 201 Created, it should include a Location header with the URI of the newly created resource (e.g. /api/posts/1). This lets clients immediately know where to find the new resource without parsing the body.",
},
],
"related_codes": ["201"],
},
{
"id": "500-validation-error",
"difficulty": "beginner",
"category": "errors",
"title": "500 for Client Mistake",
"description": "A client submits a form with an invalid email address.",
"request": 'POST /api/register HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\n\n{"name": "Alice", "email": "not-an-email"}',
"response": 'HTTP/1.1 500 Internal Server Error\nContent-Type: application/json\n\n{"error": "Invalid email format"}',
"bugs": [
{
"id": "wrong-status-class",
"description": "Should be a 4xx client error (400 or 422), not 500",
"explanation": "A 500 Internal Server Error means the server has a bug. Validation failures are client errors -- the client sent bad data. Use 400 Bad Request or 422 Unprocessable Entity to correctly signal that the client needs to fix their input.",
},
],
"related_codes": ["500", "400", "422"],
},
{
"id": "403-instead-of-401",
"difficulty": "beginner",
"category": "auth",
"title": "403 for Unauthenticated Request",
"description": "A client calls a protected endpoint without any credentials.",
"request": "GET /api/profile HTTP/1.1\nHost: api.example.com\nAccept: application/json",
"response": 'HTTP/1.1 403 Forbidden\nContent-Type: application/json\n\n{"error": "Access denied"}',
"bugs": [
{
"id": "should-be-401",
"description": "Should be 401 Unauthorized, not 403 Forbidden",
"explanation": "403 Forbidden means the server knows who you are but denies access. When no credentials are provided at all, 401 Unauthorized is correct -- it tells the client to authenticate. 403 implies the identity is known but insufficient.",
},
],
"related_codes": ["401", "403"],
},
{
"id": "302-post-form",
"difficulty": "beginner",
"category": "redirects",
"title": "POST Redirect Loses Body",
"description": "A client submits a contact form. The form handler has moved to a new URL.",
"request": 'POST /contact HTTP/1.1\nHost: www.example.com\nContent-Type: application/x-www-form-urlencoded\n\nname=Alice&message=Hello',
"response": "HTTP/1.1 302 Found\nLocation: /new-contact",
"bugs": [
{
"id": "302-drops-post",
"description": "302 may cause the browser to change POST to GET, losing the form data",
"explanation": "Browsers historically change POST to GET when following a 302. Use 307 Temporary Redirect to guarantee the POST method and body are preserved. If the move is permanent, use 308.",
},
],
"related_codes": ["302", "307", "308"],
},
{
"id": "json-html-content-type",
"difficulty": "beginner",
"category": "headers",
"title": "Wrong Content-Type for JSON",
"description": "An API returns user data in JSON format.",
"request": "GET /api/users/1 HTTP/1.1\nHost: api.example.com\nAccept: application/json",
"response": 'HTTP/1.1 200 OK\nContent-Type: text/plain\n\n{"id": 1, "name": "Alice"}',
"bugs": [
{
"id": "wrong-json-content-type",
"description": "Content-Type should be application/json, not text/plain",
"explanation": "When the response body is JSON, the Content-Type must be application/json. Using text/plain causes clients to treat the response as plain text, breaking automatic JSON parsing in HTTP libraries and fetch APIs.",
},
],
"related_codes": ["200"],
},
{
"id": "404-for-method",
"difficulty": "beginner",
"category": "api-design",
"title": "404 for Wrong Method",
"description": "A client sends a PATCH request to an endpoint that only supports GET and POST.",
"request": "PATCH /api/items/5 HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\nAuthorization: Bearer eyJhbG...\n\n{\"name\": \"updated\"}",
"response": 'HTTP/1.1 404 Not Found\nContent-Type: application/json\n\n{"error": "Not found"}',
"bugs": [
{
"id": "should-be-405",
"description": "Should be 405 Method Not Allowed, not 404",
"explanation": "The resource exists at /api/items/5, but it doesn't support PATCH. 405 Method Not Allowed is the correct status, and the response must include an Allow header listing valid methods (e.g., Allow: GET, POST).",
},
],
"related_codes": ["404", "405"],
},
# --- Intermediate (10 exercises) ---
{
"id": "429-no-retry-after",
"difficulty": "intermediate",
"category": "api-design",
"title": "429 Without Retry-After",
"description": "A client exceeds the API rate limit.",
"request": "GET /api/search?q=parrots HTTP/1.1\nHost: api.example.com\nAuthorization: Bearer eyJhbG...",
"response": 'HTTP/1.1 429 Too Many Requests\nContent-Type: application/json\n\n{"error": "Rate limit exceeded"}',
"bugs": [
{
"id": "missing-retry-after",
"description": "Missing Retry-After header",
"explanation": "Without a Retry-After header, the client has no idea when to retry. Well-behaved clients will use exponential backoff, but providing Retry-After (e.g. 60 seconds) gives concrete guidance and reduces unnecessary retry traffic.",
},
{
"id": "missing-rate-limit-headers",
"description": "Missing rate limit information headers",
"explanation": "Best practice is to include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers so clients can proactively manage their request rate instead of waiting to be throttled.",
},
],
"related_codes": ["429"],
},
{
"id": "post-301-redirect",
"difficulty": "intermediate",
"category": "redirects",
"title": "POST Redirect Uses 301",
"description": "A client submits a form via POST. The endpoint has moved to a new URL.",
"request": 'POST /api/v1/submit HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\n\n{"data": "important payload"}',
"response": "HTTP/1.1 301 Moved Permanently\nLocation: https://api.example.com/api/v2/submit\nContent-Length: 0",
"bugs": [
{
"id": "301-drops-method",
"description": "301 may cause clients to change POST to GET on redirect",
"explanation": "Historically, browsers changed POST to GET when following 301 redirects. Use 308 Permanent Redirect instead, which guarantees the HTTP method is preserved. The client's POST body will be re-sent to the new URL.",
},
],
"related_codes": ["301", "308", "307"],
},
{
"id": "set-cookie-insecure",
"difficulty": "intermediate",
"category": "auth",
"title": "Insecure Set-Cookie",
"description": "A user logs into a web application over HTTPS. The server sets a session cookie.",
"request": "POST /login HTTP/1.1\nHost: secure.example.com\nContent-Type: application/x-www-form-urlencoded\n\nusername=alice&password=secret",
"response": "HTTP/1.1 302 Found\nLocation: /dashboard\nSet-Cookie: session=abc123xyz",
"bugs": [
{
"id": "no-secure-flag",
"description": "Cookie missing Secure flag",
"explanation": "Without the Secure flag, the session cookie can be sent over unencrypted HTTP connections, making it vulnerable to interception via man-in-the-middle attacks.",
},
{
"id": "no-httponly-flag",
"description": "Cookie missing HttpOnly flag",
"explanation": "Without HttpOnly, JavaScript can access the cookie via document.cookie, making it vulnerable to XSS (cross-site scripting) attacks that steal session tokens.",
},
{
"id": "no-samesite-flag",
"description": "Cookie missing SameSite attribute",
"explanation": "Without SameSite, the cookie will be sent with cross-site requests, making it vulnerable to CSRF (cross-site request forgery) attacks. Use SameSite=Lax or SameSite=Strict.",
},
],
"related_codes": ["302"],
},
{
"id": "cors-missing-origin",
"difficulty": "intermediate",
"category": "headers",
"title": "CORS Missing Allow-Origin",
"description": "A frontend app on app.example.com makes a cross-origin API request. The server handles CORS but the response is incomplete.",
"request": "GET /api/data HTTP/1.1\nHost: api.example.com\nOrigin: https://app.example.com\nAccept: application/json",
"response": 'HTTP/1.1 200 OK\nContent-Type: application/json\nAccess-Control-Allow-Methods: GET, POST\nAccess-Control-Allow-Headers: Content-Type\n\n{"data": "here"}',
"bugs": [
{
"id": "missing-allow-origin",
"description": "Missing Access-Control-Allow-Origin header",
"explanation": "Without Access-Control-Allow-Origin, the browser will block the response from being read by the frontend JavaScript. The server must echo back the allowed origin or use * (for public APIs).",
},
],
"related_codes": ["200"],
},
{
"id": "405-no-allow",
"difficulty": "intermediate",
"category": "api-design",
"title": "405 Without Allow Header",
"description": "A client tries to DELETE a read-only resource.",
"request": "DELETE /api/system/health HTTP/1.1\nHost: api.example.com\nAuthorization: Bearer eyJhbG...",
"response": 'HTTP/1.1 405 Method Not Allowed\nContent-Type: application/json\n\n{"error": "DELETE is not allowed on this endpoint"}',
"bugs": [
{
"id": "missing-allow-header",
"description": "Missing required Allow header listing permitted methods",
"explanation": "RFC 9110 requires a 405 response to include an Allow header listing the methods the resource supports (e.g. Allow: GET, HEAD). Without it, the client must guess which methods are valid.",
},
],
"related_codes": ["405"],
},
{
"id": "cache-immutable-revalidate",
"difficulty": "intermediate",
"category": "caching",
"title": "Contradictory Cache Headers",
"description": "A server returns a static asset (a versioned JavaScript file) with conflicting cache directives.",
"request": "GET /static/app.v3.2.1.js HTTP/1.1\nHost: cdn.example.com\nAccept: */*",
"response": "HTTP/1.1 200 OK\nContent-Type: application/javascript\nCache-Control: no-cache, immutable, max-age=31536000\nETag: \"v3.2.1\"",
"bugs": [
{
"id": "contradictory-cache",
"description": "no-cache contradicts immutable and max-age",
"explanation": "no-cache forces revalidation on every request, while immutable tells clients the resource will never change. These are contradictory. For versioned static assets, use Cache-Control: public, max-age=31536000, immutable without no-cache.",
},
],
"related_codes": ["200", "304"],
},
{
"id": "content-negotiation-ignore",
"difficulty": "intermediate",
"category": "headers",
"title": "Accept Header Ignored",
"description": "A client requests XML, but the server returns JSON without informing the client.",
"request": "GET /api/reports/42 HTTP/1.1\nHost: api.example.com\nAccept: application/xml\nAuthorization: Bearer eyJhbG...",
"response": 'HTTP/1.1 200 OK\nContent-Type: application/json\n\n{"id": 42, "title": "Q4 Report", "data": [1, 2, 3]}',
"bugs": [
{
"id": "accept-header-violated",
"description": "Server returns JSON despite client requesting XML",
"explanation": "The client's Accept header says application/xml but the server returns JSON. If the server cannot produce XML, it should return 406 Not Acceptable, not silently send a different format. Clients relying on XML parsing will break.",
},
],
"related_codes": ["200", "406"],
},
{
"id": "oauth-redirect-wrong-code",
"difficulty": "intermediate",
"category": "auth",
"title": "OAuth Redirect Error",
"description": "An OAuth authorization server redirects the user back to the client app after granting consent.",
"request": "GET /authorize?response_type=code&client_id=abc&redirect_uri=https://app.example.com/callback&state=xyz123 HTTP/1.1\nHost: auth.example.com\nCookie: session=logged-in-user",
"response": "HTTP/1.1 301 Moved Permanently\nLocation: https://app.example.com/callback?code=AUTH_CODE_123&state=xyz123",
"bugs": [
{
"id": "wrong-redirect-status",
"description": "Should use 302 Found, not 301 Moved Permanently",
"explanation": "OAuth 2.0 (RFC 6749) requires 302 Found for the authorization redirect. Using 301 causes browsers to cache the redirect permanently, so future authorization requests skip the auth server entirely and reuse the old (likely expired) authorization code.",
},
],
"related_codes": ["301", "302"],
},
{
"id": "jwt-query-string",
"difficulty": "intermediate",
"category": "auth",
"title": "JWT in Query String",
"description": "A client accesses a protected API endpoint, passing the JWT token in the URL.",
"request": "GET /api/account?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U HTTP/1.1\nHost: api.example.com\nAccept: application/json",
"response": 'HTTP/1.1 200 OK\nContent-Type: application/json\n\n{"account": "premium", "balance": 500}',
"bugs": [
{
"id": "jwt-in-url",
"description": "JWT should be in the Authorization header, not the query string",
"explanation": "Putting tokens in URLs is a security risk: URLs are logged in server access logs, browser history, proxy logs, and Referer headers. JWTs should be sent in the Authorization: Bearer header. The server should reject tokens in query strings or at minimum not encourage this pattern.",
},
],
"related_codes": ["200", "401"],
},
{
"id": "api-version-url-mismatch",
"difficulty": "intermediate",
"category": "api-design",
"title": "API Versioning Mismatch",
"description": "A client calls v2 of the API, but the server returns a v1-format response with different field names.",
"request": "GET /api/v2/users/42 HTTP/1.1\nHost: api.example.com\nAccept: application/json\nAuthorization: Bearer eyJhbG...",
"response": 'HTTP/1.1 200 OK\nContent-Type: application/json\nX-API-Version: v1\n\n{"user_name": "Alice", "email_addr": "alice@example.com"}',
"bugs": [
{
"id": "version-mismatch",
"description": "X-API-Version says v1 but the client requested v2",
"explanation": "The client explicitly requested /api/v2/ but the server returned a v1 response with v1 field names (user_name instead of v2's username). This breaks client code expecting v2 format. The server should either return v2 format or respond with 404/410 if v2 doesn't exist.",
},
],
"related_codes": ["200", "404", "410"],
},
# --- Expert (11 exercises) ---
{
"id": "preflight-missing-max-age",
"difficulty": "expert",
"category": "headers",
"title": "CORS Preflight Incomplete",
"description": "A browser sends a CORS preflight request for a cross-origin POST with a JSON body. The server responds, but the response has issues.",
"request": "OPTIONS /api/data HTTP/1.1\nHost: api.example.com\nOrigin: https://app.example.com\nAccess-Control-Request-Method: POST\nAccess-Control-Request-Headers: Content-Type, Authorization",
"response": "HTTP/1.1 200 OK\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Methods: GET, POST\nAccess-Control-Allow-Headers: Content-Type\nContent-Length: 0",
"bugs": [
{
"id": "wildcard-with-credentials",
"description": "Wildcard origin (*) will fail if the request includes credentials",
"explanation": "If the actual request sends cookies or Authorization headers, the browser requires Access-Control-Allow-Origin to echo the exact origin, not *. The wildcard blocks credentialed requests entirely.",
},
{
"id": "missing-auth-header-in-allow",
"description": "Authorization header not listed in Access-Control-Allow-Headers",
"explanation": "The preflight requested permission for both Content-Type and Authorization, but the server only allowed Content-Type. The browser will block the actual request because Authorization was not approved.",
},
{
"id": "no-max-age",
"description": "Missing Access-Control-Max-Age header",
"explanation": "Without Access-Control-Max-Age, the browser sends a preflight OPTIONS request before every single cross-origin request. Setting a max-age (e.g. 86400) lets browsers cache the preflight result and skip redundant requests.",
},
],
"related_codes": ["200"],
},
{
"id": "206-wrong-content-range",
"difficulty": "expert",
"category": "caching",
"title": "Partial Content Mismatch",
"description": "A client requests a byte range of a large video file. The server returns partial content but the headers are inconsistent.",
"request": "GET /videos/movie.mp4 HTTP/1.1\nHost: cdn.example.com\nRange: bytes=1000-1999",
"response": "HTTP/1.1 206 Partial Content\nContent-Type: video/mp4\nContent-Range: bytes 1000-1999/50000\nContent-Length: 500\nAccept-Ranges: bytes\n\n[binary data]",
"bugs": [
{
"id": "content-length-mismatch",
"description": "Content-Length (500) does not match the range size (1000 bytes)",
"explanation": "The range 1000-1999 is 1000 bytes, but Content-Length says 500. This mismatch will cause clients to either truncate data or hang waiting for more bytes. Content-Length must equal the actual number of bytes in the range.",
},
],
"related_codes": ["206", "416"],
},
{
"id": "412-missing-etag",
"difficulty": "expert",
"category": "caching",
"title": "412 Without Current State",
"description": "A client tries to update a document using a conditional request, but the precondition fails.",
"request": 'PUT /api/documents/88 HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\nIf-Match: "v5"\nAuthorization: Bearer eyJhbG...\n\n{"title": "Updated Title"}',
"response": 'HTTP/1.1 412 Precondition Failed\nContent-Type: application/json\n\n{"error": "Precondition failed"}',
"bugs": [
{
"id": "missing-current-etag",
"description": "Should include the current ETag so the client can retry",
"explanation": "When a 412 response occurs because If-Match failed, the server should include the current ETag header. This lets the client fetch the latest version, resolve conflicts, and retry the update without an extra round trip.",
},
],
"related_codes": ["412", "428"],
},
{
"id": "303-after-post",
"difficulty": "expert",
"category": "crud",
"title": "POST Success Returns 200",
"description": "A user submits an order form. The server processes it and returns the order confirmation page directly.",
"request": 'POST /checkout HTTP/1.1\nHost: shop.example.com\nContent-Type: application/x-www-form-urlencoded\nCookie: session=xyz789\n\nitem=widget&qty=3&card=tok_visa',
"response": 'HTTP/1.1 200 OK\nContent-Type: text/html\n\n<html><body><h1>Order Confirmed!</h1><p>Order #1024 placed.</p></body></html>',
"bugs": [
{
"id": "should-use-prg",
"description": "Should use Post/Redirect/Get pattern (303 See Other) instead of 200",
"explanation": "Returning 200 directly after a POST means the browser's URL still shows /checkout. If the user refreshes, the browser will resubmit the POST (duplicate order!). Use 303 See Other to redirect to /orders/1024 -- this is the Post/Redirect/Get pattern that prevents accidental resubmission.",
},
],
"related_codes": ["303", "302"],
},
{
"id": "json-wrong-content-type",
"difficulty": "expert",
"category": "headers",
"title": "JSON with Wrong Content-Type",
"description": "An API returns a JSON response, but something is off about the headers.",
"request": "GET /api/users/42 HTTP/1.1\nHost: api.example.com\nAccept: application/json\nAuthorization: Bearer eyJhbG...",
"response": 'HTTP/1.1 200 OK\nContent-Type: text/html\nX-Powered-By: Express\n\n{"id": 42, "name": "Alice", "email": "alice@example.com"}',
"bugs": [
{
"id": "wrong-content-type",
"description": "Content-Type is text/html but body is JSON",
"explanation": "When the Content-Type says text/html but the body is JSON, browsers may try to render it as HTML (XSS risk), and API clients may fail to parse it. Use application/json for JSON responses.",
},
{
"id": "leaking-server-info",
"description": "X-Powered-By header leaks server technology",
"explanation": "The X-Powered-By header reveals the server framework (Express). This information helps attackers target known vulnerabilities. Remove it in production.",
},
],
"related_codes": ["200"],
},
{
"id": "hsts-missing-on-https",
"difficulty": "expert",
"category": "auth",
"title": "HTTPS Without HSTS",
"description": "A banking API serves all traffic over HTTPS but is missing a key security header.",
"request": "GET /api/account/balance HTTP/1.1\nHost: bank-api.example.com\nAccept: application/json\nAuthorization: Bearer eyJhbG...",
"response": 'HTTP/1.1 200 OK\nContent-Type: application/json\nCache-Control: no-store\n\n{"balance": 12345.67, "currency": "USD"}',
"bugs": [
{
"id": "missing-hsts",
"description": "Missing Strict-Transport-Security (HSTS) header",
"explanation": "Without HSTS, a user's first visit (or after the browser cache expires) could be intercepted and downgraded to HTTP via an SSL stripping attack. Add Strict-Transport-Security: max-age=31536000; includeSubDomains to tell browsers to always use HTTPS.",
},
{
"id": "caching-sensitive-data",
"description": "no-store is correct, but should also add private",
"explanation": "For sensitive financial data, Cache-Control: no-store, private ensures that neither the browser nor any intermediate proxy or CDN caches the response. While no-store is the main directive, adding private is defense-in-depth.",
},
],
"related_codes": ["200"],
},
{
"id": "graphql-wrong-status",
"difficulty": "expert",
"category": "api-design",
"title": "GraphQL Error Returns 400",
"description": "A client sends a valid HTTP POST with a GraphQL query that references a non-existent field.",
"request": 'POST /graphql HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\nAuthorization: Bearer eyJhbG...\n\n{"query": "{ user(id: 1) { name nonExistentField } }"}',
"response": 'HTTP/1.1 400 Bad Request\nContent-Type: application/json\n\n{"errors": [{"message": "Cannot query field nonExistentField on type User"}]}',
"bugs": [
{
"id": "graphql-should-be-200",
"description": "Should return 200 OK with errors in the response body",
"explanation": "Per the GraphQL over HTTP spec, query validation errors should still return 200 OK. The HTTP status reflects transport-level success, while GraphQL errors go in the 'errors' array. 400 is only for malformed HTTP requests (e.g., invalid JSON). This distinction matters because HTTP middleware and CDNs may cache or retry based on status codes.",
},
],
"related_codes": ["200", "400"],
},
{
"id": "webhook-no-idempotency",
"difficulty": "expert",
"category": "api-design",
"title": "Webhook Missing Idempotency Key",
"description": "A payment provider sends a webhook notification about a completed payment. The webhook endpoint processes it.",
"request": 'POST /webhooks/payment HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\nX-Signature: sha256=abc123...\n\n{"event": "payment.completed", "amount": 99.99, "order_id": "ORD-001"}',
"response": 'HTTP/1.1 200 OK\nContent-Type: application/json\n\n{"processed": true}',
"bugs": [
{
"id": "no-idempotency-handling",
"description": "No idempotency key in the response or evidence of deduplication",
"explanation": "Webhook providers may retry delivery if they don't receive a timely response. Without idempotency handling (checking an event ID to prevent double-processing), the same payment could be processed multiple times. The response should acknowledge the specific event ID.",
},
{
"id": "response-too-verbose",
"description": "Webhook responses should be minimal -- just a 2xx status",
"explanation": "Webhook senders typically only check the status code. Returning detailed JSON wastes bandwidth and could leak internal processing details. A simple 200 or 204 with no body is standard practice.",
},
],
"related_codes": ["200", "204"],
},
{
"id": "sse-wrong-content-type",
"difficulty": "expert",
"category": "api-design",
"title": "SSE Wrong Content-Type",
"description": "A server sets up a Server-Sent Events stream for real-time notifications.",
"request": "GET /events/notifications HTTP/1.1\nHost: api.example.com\nAccept: text/event-stream\nAuthorization: Bearer eyJhbG...",
"response": "HTTP/1.1 200 OK\nContent-Type: application/json\nCache-Control: no-cache\nConnection: keep-alive\n\ndata: {\"type\": \"message\", \"text\": \"Hello\"}\n\ndata: {\"type\": \"message\", \"text\": \"World\"}\n\n",
"bugs": [
{
"id": "sse-wrong-content-type",
"description": "Content-Type should be text/event-stream, not application/json",
"explanation": "Server-Sent Events require Content-Type: text/event-stream. Using application/json prevents the browser's EventSource API from processing the stream. The EventSource constructor will throw an error if the MIME type is wrong.",
},
{
"id": "sse-missing-retry",
"description": "Should include a retry field for automatic reconnection",
"explanation": "SSE streams should include a retry: field (e.g., retry: 3000) to tell the browser how long to wait before reconnecting after a disconnect. Without it, the browser uses its default (often too aggressive or too slow).",
},
],
"related_codes": ["200"],
},
{
"id": "streaming-content-length",
"difficulty": "expert",
"category": "headers",
"title": "Streaming with Content-Length",
"description": "A server streams a dynamically generated CSV export. The file size is unknown upfront.",
"request": "GET /api/export/users.csv HTTP/1.1\nHost: api.example.com\nAccept: text/csv\nAuthorization: Bearer eyJhbG...",
"response": "HTTP/1.1 200 OK\nContent-Type: text/csv\nContent-Length: 0\nContent-Disposition: attachment; filename=\"users.csv\"\n\nid,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com",
"bugs": [
{
"id": "content-length-for-stream",
"description": "Content-Length: 0 is wrong for a response with body data",
"explanation": "Setting Content-Length: 0 for a streaming response tells the client to expect no data. For dynamically generated content of unknown size, use Transfer-Encoding: chunked instead of Content-Length, allowing the server to stream data in chunks.",
},
],
"related_codes": ["200"],
},
{
"id": "conflict-no-current-state",
"difficulty": "expert",
"category": "crud",
"title": "409 Without Conflict Details",
"description": "A client tries to update a record, but it conflicts with a concurrent modification.",
"request": 'PUT /api/documents/55 HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\nIf-Match: "etag-old"\nAuthorization: Bearer eyJhbG...\n\n{"title": "My Updated Doc"}',
"response": 'HTTP/1.1 409 Conflict\nContent-Type: application/json\n\n{"error": "Conflict"}',
"bugs": [
{
"id": "no-conflict-details",
"description": "Response should include the current state or ETag for conflict resolution",
"explanation": "A 409 Conflict without details forces the client to make an extra GET request to fetch the current state. Including the current ETag and ideally the conflicting resource state lets the client resolve the conflict in one fewer round trip.",
},
{
"id": "should-be-412",
"description": "When If-Match fails, 412 Precondition Failed is more precise than 409",
"explanation": "Since the client used If-Match, the server should return 412 Precondition Failed (the conditional header failed) rather than the more general 409 Conflict. 412 specifically tells the client their ETag-based precondition was not met.",
},
],
"related_codes": ["409", "412"],
},
{
"id": "308-missing-location",
"difficulty": "beginner",
"category": "redirects",
"title": "308 Without Location",
"description": "A permanent redirect that doesn't say where to go.",
"request": "POST /api/v1/submit HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\n\n{\"data\": \"test\"}",
"response": "HTTP/1.1 308 Permanent Redirect\nContent-Type: text/html\nContent-Length: 0",
"bugs": [
{
"id": "missing-location-308",
"description": "Missing Location header for redirect",
"explanation": "A 308 Permanent Redirect without a Location header is useless — the client has no idea where to resend the request. Always include Location with any 3xx redirect.",
},
],
"related_codes": ["308", "301"],
},
{
"id": "201-missing-body",
"difficulty": "intermediate",
"category": "crud",
"title": "201 Created Without Resource",
"description": "A new resource is created but the response doesn't include it or its location.",
"request": "POST /api/users HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\n\n{\"name\": \"Alice\", \"email\": \"alice@example.com\"}",
"response": "HTTP/1.1 201 Created\nContent-Type: application/json\nContent-Length: 0",
"bugs": [
{
"id": "no-body-or-location",
"description": "201 should include the created resource or a Location header",
"explanation": "A 201 Created response should either return the created resource in the body OR include a Location header pointing to it. Without either, the client doesn't know the ID or URL of what was created.",
},
],
"related_codes": ["201", "200"],
},
{
"id": "302-method-change",
"difficulty": "intermediate",
"category": "redirects",
"title": "302 Changing POST to GET",
"description": "A form submits data via POST, but after the redirect, the data is lost because the browser changed the method.",
"request": "POST /api/submit HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\n\n{\"name\": \"Alice\"}",
"response": "HTTP/1.1 302 Found\nLocation: /api/process\nContent-Length: 0",
"bugs": [
{
"id": "method-not-preserved",
"description": "302 may change POST to GET — use 307 to preserve method",
"explanation": "Historically, browsers changed POST to GET on 302 redirects. If the form data must reach the redirect target, use 307 Temporary Redirect which guarantees the method is preserved.",
},
],
"related_codes": ["302", "307"],
},
{
"id": "429-aggressive-retry",
"difficulty": "expert",
"category": "api-design",
"title": "429 Without Backoff Guidance",
"description": "An API rate-limits clients but doesn't provide enough information for proper retry behavior.",
"request": "GET /api/data HTTP/1.1\nHost: api.example.com\nAuthorization: Bearer eyJhbG...",
"response": "HTTP/1.1 429 Too Many Requests\nContent-Type: application/json\n\n{\"error\": \"rate limit exceeded\", \"limit\": 100, \"window\": \"1m\"}",
"bugs": [
{
"id": "missing-retry-after-header",
"description": "Missing Retry-After header",
"explanation": "While the body mentions the rate limit, the Retry-After header is the standard mechanism for telling clients when to retry. Without it, clients may retry immediately, worsening the overload. Add: Retry-After: 60",
},
{
"id": "missing-ratelimit-headers",
"description": "Missing X-RateLimit-* headers for proactive throttling",
"explanation": "Best practice is to include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers so clients can throttle proactively before hitting the limit.",
},
],
"related_codes": ["429", "503"],
},
{
"id": "200-wrong-content-type",
"difficulty": "beginner",
"category": "crud",
"title": "JSON Response with Wrong Content-Type",
"description": "A client expects JSON but the response header says otherwise.",
"request": "GET /api/users/42 HTTP/1.1\nHost: api.example.com\nAccept: application/json",
"response": "HTTP/1.1 200 OK\nContent-Type: text/html\n\n{\"id\": 42, \"name\": \"Alice\"}",
"bugs": [
{
"id": "content-type-mismatch",
"description": "Content-Type says text/html but body is JSON",
"explanation": "The response body is valid JSON but the Content-Type header says text/html. Clients relying on the Content-Type will try to render it as HTML instead of parsing it as JSON. Set Content-Type: application/json.",
},
],
"related_codes": ["200", "406"],
},
{
"id": "307-location-missing",
"difficulty": "beginner",
"category": "redirects",
"title": "307 Without Location",
"description": "An API temporarily redirects a POST request but forgets to tell the client where to go.",
"request": "POST /api/v1/submit HTTP/1.1\nHost: api.example.com\nContent-Type: application/json\n\n{\"data\": \"test\"}",
"response": "HTTP/1.1 307 Temporary Redirect\nContent-Length: 0",
"bugs": [
{
"id": "307-no-location",
"description": "Missing Location header",
"explanation": "A 307 Temporary Redirect must include a Location header. Without it, the client doesn't know where to resend the POST request. The body and method are preserved, but there's nowhere to send them.",
},
],
"related_codes": ["307", "302"],
},
{
"id": "403-leaking-existence",
"difficulty": "intermediate",
"category": "security",
"title": "403 Leaking Resource Existence",
"description": "A security-sensitive API reveals that a resource exists to unauthorized users.",
"request": "GET /api/admin/users HTTP/1.1\nHost: api.example.com",
"response": "HTTP/1.1 403 Forbidden\nContent-Type: application/json\n\n{\"error\": \"You are not authorized to view admin users\"}",
"bugs": [
{
"id": "existence-leak",
"description": "Should return 404 to hide resource existence",
"explanation": "Returning 403 confirms that /api/admin/users exists. An attacker now knows this endpoint is real. For security-sensitive endpoints, return 404 Not Found to prevent information disclosure. This is called 'endpoint enumeration prevention'.",
},
],
"related_codes": ["403", "404"],
},
]