================================================================================ CRUNCHYROLL DOWNLOADER - CHALLENGE 3 SOLUTION Chapter 1: JWT & API Setup Challenge: HTTP Headers with JWT ================================================================================ PROBLEM: Make authenticated HTTP requests: 1. Create sample JWT token 2. Create HTTP headers with Authorization: Bearer {token} 3. Test request to sample API 4. Verify token is sent correctly SOLUTION: ================================================================================ File: test_jwt_headers.py import requests def make_authenticated_request(url: str, token: str) -> dict: """Make HTTP request with JWT token in header.""" print("=" * 70) print("AUTHENTICATED HTTP REQUEST TEST") print("=" * 70) # Create headers with JWT token headers = { "Authorization": f"Bearer {token}", "User-Agent": "Mozilla/5.0", "Content-Type": "application/json" } print(f"\nšŸ“¤ REQUEST DETAILS:") print(f" URL: {url}") print(f" Method: GET") print(f"\nšŸ“‹ HEADERS:") print(f" Authorization: Bearer {token[:30]}...") print(f" User-Agent: {headers['User-Agent']}") print(f" Content-Type: {headers['Content-Type']}") try: # Make request with headers print(f"\nšŸ”„ Sending request...") response = requests.get(url, headers=headers, timeout=10) print(f"\nšŸ“„ RESPONSE:") print(f" Status Code: {response.status_code}") print(f" Content-Type: {response.headers.get('content-type')}") # Check response if response.status_code == 200: print(f" āœ… Request successful!") data = response.json() return data elif response.status_code == 401: print(f" āŒ Unauthorized - token invalid or expired") return None elif response.status_code == 403: print(f" āŒ Forbidden - no permission for this endpoint") return None else: print(f" āš ļø Unexpected status code") return None except requests.exceptions.Timeout: print(f" āŒ Request timeout") return None except requests.exceptions.ConnectionError: print(f" āŒ Connection error") return None except Exception as e: print(f" āŒ Error: {e}") return None # Test with public API (doesn't require real token) if __name__ == "__main__": # Test 1: Request WITHOUT token (will fail) print("\n" + "=" * 70) print("TEST 1: Request WITHOUT token (should fail with 401)") print("=" * 70) url = "https://api.crunchyroll.com/api/v2/episodes" try: response = requests.get(url, timeout=5) print(f"Status: {response.status_code}") if response.status_code == 401: print("āœ… Expected: Server rejected request without token") except Exception as e: print(f"Could not test (API may not allow): {e}") # Test 2: Request WITH token (will fail with invalid token, but format is correct) print("\n" + "=" * 70) print("TEST 2: Request WITH token (correct format)") print("=" * 70) fake_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U" result = make_authenticated_request(url, fake_token) if result: print(f"\nāœ… Got response: {result}") else: print(f"\n(This is expected with invalid token)") HOW JWT HEADERS WORK ================================================================================ When you make a request WITH JWT: CLIENT: ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ GET /api/v2/episodes HTTP/1.1 │ │ Host: api.crunchyroll.com │ │ Authorization: Bearer eyJhbGciOi... │ ← JWT token here! │ User-Agent: Mozilla/5.0 │ │ Content-Type: application/json │ │ │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ↓ SERVER: ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ 1. Extract token from Authorization │ │ 2. Verify token signature │ │ 3. Check if token expired │ │ 4. Extract user info from payload │ │ │ │ If valid: Return data (200 OK) │ │ If invalid: Return error (401) │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ STATUS CODES YOU'LL SEE ================================================================================ 200 OK: āœ… Request successful āœ… Token valid āœ… Got data 401 Unauthorized: āŒ No token provided OR āŒ Token invalid OR āŒ Token expired 403 Forbidden: āš ļø Token valid but user doesn't have permission 429 Too Many Requests: āš ļø Rate limiting - wait before next request 500 Internal Server Error: āš ļø Server problem (temporary) REAL USAGE IN CRUNCHYROLL DOWNLOADER ================================================================================ from crunchyroll_utils import login, get_with_token from config import Config # Step 1: Login to get token config = Config() token = login(config) # Step 2: Use token for authenticated requests url = "https://api.crunchyroll.com/api/v2/episodes" episodes = get_with_token(url, token) # Step 3: Process episodes if episodes: print(f"Got {len(episodes)} episodes") else: print("Failed to get episodes") KEY POINTS ================================================================================ āœ… Authorization header format: "Bearer {token}" (note the space!) āœ… Token goes in HEADER, not in URL or body āœ… Each request needs token āœ… If token expires, must login again āœ… Can cache token to avoid re-login Common mistake: āŒ "Authorization: {token}" (missing "Bearer ") āŒ "Authorization: Bearer{token}" (missing space) āœ… "Authorization: Bearer {token}" (correct!) ================================================================================