Preparing your learning space...
60% through Debugging & Troubleshooting tutorials
Most FDE work happens where your system meets the outside world: your code calling an API, an auth flow deciding who's allowed in, a webhook arriving from a third party. Failures at that boundary are often silent or misleading. Closely tied to it is security — the discipline of checking whether that boundary is too open. This tutorial merges the two: first the playbook for debugging API calls, authentication, and integrations; then security troubleshooting for the same boundary.
When your system talks to someone else's, a failure has at least three possible homes: you sent something wrong, they didn't do what you expected, or the connection between you broke. All three look alike from the outside — "the API failed." Your job is to find out which layer is lying, then to check whether the boundary is as closed as it should be.
Why it is useful: every integration ticket is a cross-examination. Rule out layers one at a time instead of assuming the API is broken.
Everything external reduces to one HTTP request and one HTTP response. If you can see both in full, you can debug anything.
Request: METHOD URL Headers (auth, content-type, ...) Body Response: Status code Headers (rate-limit, retry-after, ...) Body
Explanation: capture the full request and the full response before changing anything.
Given "the API is failing," run through this order — it's fast and it rules out layers:
401/403 needs no deep debugging beyond token and scope.200 that still fails means you parsed something wrong.5xx.Work top to bottom. Most "API is broken" tickets are actually layers 1–3.
An API fails on the whole thing you sent, and it can fail for one line. Change the HTTP call, not your entire integration.
import requests, os
resp = requests.get(
"https://api.example.com/v1/orders", # wrong path? missing version?
params={"status": "open"}, # wrong query param name?
headers={"Authorization": f"Bearer {os.getenv('TOKEN')}"},
timeout=30,
)
print(resp.status_code, resp.text[:500])
Explanation: when the call returns something unexpected, print the status and body — they tell you what the server actually did. A 400/404 usually means you changed the request shape; a 401 means auth; a 5xx means their side or a bug you triggered.
Best Practice: reproduce the failing call exactly and diff it against a known-good call. The difference is almost always the bug.
HTTP status codes are a short language you read fast:
| Status | Meaning | Likely problem layer |
|---|---|---|
| 2xx | success | none — the bug is in your parsing |
| 400 | bad request | your request shape |
| 401 | unauthenticated | your auth (missing or expired credential) |
| 403 | forbidden | auth is valid but lacks scope or permission |
| 404 | not found | wrong URL, or resource truly absent |
| 429 | rate limited | you exceeded the quota |
| 5xx | server error | their side (or a bug you triggered) |
Why it is useful: the status narrows the hunt instantly. A 403 and a 500 are entirely different problems — you shouldn't be reading logs for one of them.
At every important API boundary, log these fields. When a call fails in production, you'll already have the answer:
{
"method": "POST",
"url": "https://api.crm.example.com/v1/leads",
"status": 429,
"duration_ms": 1230,
"error_code": "rate_limit_exceeded",
"retryable": true
}
Explanation: a structured log of method, URL, status, and duration makes later debugging a lookup rather than a hunt. Adding retryable and the provider's error code tells you what to do, not just what happened.
Most auth bugs come down to one line. Match the symptom:
| Symptom | Meaning | First check |
|---|---|---|
| 401 | credential missing, wrong, or expired | key valid? header name right? |
| 403 | credential valid, lacks permission | does the token carry this scope? |
| 401 on refresh | refresh token invalid | was it revoked or rotated? |
| local 200, deployed 401 | prod uses a different key | env var set in the right environment? |
When you see 401, verify the whole request first — headers included — and that the credential belongs to the environment you're testing. A prod key pasted into a dev test confuses you until you realize which API you were really hitting.
OAuth's many steps each fail on their own line. Match the error to the step:
redirect_uri doesn't match the registered one. It must match exactly, including any query string.invalid_grant → the authorization code was already used, expired, or you sent the wrong one. Codes are single-use.invalid_client → wrong client_id/client_secret, or a secret from a different app.Example — validating a token before your code even runs:
curl -s https://graph.facebook.com/debug_token \
-d "input_token=$ACCESS_TOKEN" \
-d "access_token=$APP_TOKEN"
Explanation: many providers expose a token-introspection endpoint. It tells you whether the access token is valid, which scopes it carries, and when it expires — splitting "my token is broken" from "my call is broken" in one command.
Webhooks add a twist: the other system calls you. A silent failure often means the provider never got the event, or never got a successful acknowledgement from you.
from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks/orders")
def handle_order():
payload = request.get_json()
print("received webhook:", payload) # did it even arrive?
return "ok", 200 # you must ack, or the provider retries forever
Explanation: webhook debugging is three checks: (1) did the event arrive? (2) did you ack it? (3) did your handler succeed? Print the first thing you receive, and respond with a proper 2xx on success. If you don't ack, the provider re-sends — and "duplicate events" turn out to be "unacked events."
Best Practice: learn each provider's retry rules, and verify the signature on incoming webhooks. A provider that signs its payloads gives you a way to discard forged or replayed events.
An API logging shows what happened; security troubleshooting asks whether the boundary should have let it happen. A feature that works correctly for a normal user can still be a vulnerability if it lets one user act as another or reach something private.
Why it is useful: this mindset turns "everything works, so it must be fine" into actually finding the hole.
Ask these before assuming anything is secure:
Example — an endpoint that checks for a user but trusts their id:
@app.get("/account/{uid}")
def account(uid):
# BUG: trusts the uid in the URL instead of the logged-in user
return get_user(uid)
Explanation: this lets any caller read any user's account by changing the uid in the URL. There's no check that the caller is that user. Derive identity from the session, not from a client-supplied value — this is broken authorization hiding inside a "working" endpoint.
Secrets find their way into code, logs, and public repositories. Check the obvious places:
grep -r "sk_live\|AKIA\|BEGIN PRIVATE KEY" . --include="*.py" --include="*.env*"
Explanation: this searches your tree for common secret patterns — API keys, AWS access keys, private-key headers. If anything hits, a secret is in a place it shouldn't be. Rotate it immediately and audit who saw it; don't just delete the string from the file.
Best Practice: treat a leaked secret as compromised, not just mistyped. Rotate it, and check version history and logs for copies.
If you suspect the boundary is being attacked, look for evidence in logs — repeated failures, unusual sources, or a spike in one kind of request.
grep "401" access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
Explanation: this counts failed logins per source IP and sorts them. One IP with hundreds of 401s is a brute-force pattern, not a bug. An attack leaves a pattern; find it in the logs.
Save your progress and earn XP for completing tutorials.
4 questions · Pass with 70%+
1Your API returns 403. What does this mean and where do you look?
2An OAuth flow fails with invalid_grant. Most likely cause?
3Webhooks are arriving but you see "duplicate" events. The most common real cause?
4In this snippet, what's the security flaw? @app.get("/account/{uid}") def account(uid): return get_user(uid) # no check that caller == uid
Technology
Forward Deployed Engineer
Lesson group
Debugging & Troubleshooting
Progress
60% complete