Skip to content

Commit 4d40c4a

Browse files
authored
fix: resolve Vercel serverless deployment issues (#134)
Three fixes for self-hosting on Vercel: 1. pyproject.toml: add [project] table so Vercel's uv-based Python runtime can resolve dependencies without erroring on lock. 2. api/app.py: insert api/ into sys.path before dynamic imports so sibling modules (login, callback, view) are found when the file runs as a Vercel serverless function entry-point. 3. api/callback.py + util/spotify.py: surface Spotify API errors explicitly instead of crashing with a JSONDecodeError on empty bodies. Adds get_user_profile_raw() and checks HTTP status before parsing, returning a readable 400/502 response on failure. Co-authored-by: xiaoyueyoqwq <xiaoyueyoqwq@users.noreply.github.com>
1 parent 542f039 commit 4d40c4a

4 files changed

Lines changed: 62 additions & 10 deletions

File tree

api/app.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
from flask import Flask, redirect
21
import importlib
2+
import os
3+
import sys
4+
5+
from flask import Flask, redirect
6+
7+
# Ensure the api/ directory is on sys.path so sibling modules resolve correctly
8+
# when running as a Vercel serverless function.
9+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
310

411
# Import legacy handlers (order matters for Firebase init)
512
login_module = importlib.import_module("login")

api/callback.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
from flask import Flask, Response, jsonify, render_template, redirect, request
21
from base64 import b64decode
3-
from dotenv import load_dotenv, find_dotenv
2+
3+
from dotenv import find_dotenv, load_dotenv
4+
from flask import Flask, Response, jsonify, redirect, render_template, request
45

56
load_dotenv(find_dotenv())
67

7-
from firebase_admin import credentials
8-
from firebase_admin import firestore
8+
import json
9+
import os
10+
911
import firebase_admin
12+
from firebase_admin import credentials, firestore
1013

11-
import os
12-
import json
1314
from util import spotify
1415

1516
print("Starting Server")
@@ -36,9 +37,22 @@ def catch_all(path):
3637
return Response("not ok")
3738

3839
token_info = spotify.generate_token(code)
40+
41+
if "access_token" not in token_info:
42+
error = token_info.get("error", "unknown")
43+
desc = token_info.get("error_description", "")
44+
return Response(f"Token exchange failed: {error} - {desc}", status=400)
45+
3946
access_token = token_info["access_token"]
4047

41-
spotify_user = spotify.get_user_profile(access_token)
48+
profile_resp = spotify.get_user_profile_raw(access_token)
49+
if profile_resp.status_code != 200 or not profile_resp.text.strip():
50+
return Response(
51+
f"Spotify profile fetch failed: HTTP {profile_resp.status_code} - {profile_resp.text[:300]}",
52+
status=502,
53+
)
54+
55+
spotify_user = profile_resp.json()
4256
user_id = spotify_user["id"]
4357

4458
doc_ref = db.collection("users").document(user_id)

pyproject.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
[project]
2+
name = "spotify-github-profile"
3+
version = "1.0.0"
4+
requires-python = ">=3.12"
5+
dependencies = [
6+
"Flask==3.1.2",
7+
"Werkzeug==3.1.3",
8+
"requests==2.32.5",
9+
"python-dotenv==1.1.1",
10+
"firebase-admin==7.1.0",
11+
"Pillow==11.3.0",
12+
"colorgram.py==1.2.0",
13+
"markupsafe==3.0.3",
14+
"gunicorn==23.0.0",
15+
"profanityfilter==2.1.0",
16+
]
17+
118
[tool.pytest.ini_options]
219
minversion = "6.0"
320
addopts = "-ra -q --strict-markers --strict-config"

util/spotify.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from base64 import b64encode
22

3-
from dotenv import load_dotenv, find_dotenv
3+
from dotenv import find_dotenv, load_dotenv
44

55
load_dotenv(find_dotenv())
66

7-
import requests
87
import json
98
import os
109
import random
1110

11+
import requests
12+
1213
SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
1314
SPOTIFY_SECRET_ID = os.getenv("SPOTIFY_SECRET_ID")
1415
BASE_URL = os.getenv("BASE_URL")
@@ -25,15 +26,18 @@
2526
SPOTIFY_URL_GENERATE_TOKEN = "https://accounts.spotify.com/api/token"
2627
SPOTIFY_URL_USER_INFO = "https://api.spotify.com/v1/me"
2728

29+
2830
class InvalidTokenError(Exception):
2931
pass
3032

33+
3134
def get_authorization():
3235

3336
return b64encode(f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_SECRET_ID}".encode()).decode(
3437
"ascii"
3538
)
3639

40+
3741
def generate_token(authorization_code):
3842

3943
data = {
@@ -49,6 +53,7 @@ def generate_token(authorization_code):
4953

5054
return response_json
5155

56+
5257
def refresh_token(refresh_token):
5358

5459
data = {
@@ -63,6 +68,13 @@ def refresh_token(refresh_token):
6368

6469
return response_json
6570

71+
72+
def get_user_profile_raw(access_token):
73+
"""Return the raw Response object for flexible error handling by callers."""
74+
headers = {"Authorization": f"Bearer {access_token}"}
75+
return requests.get(SPOTIFY_URL_USER_INFO, headers=headers)
76+
77+
6678
def get_user_profile(access_token):
6779

6880
headers = {"Authorization": f"Bearer {access_token}"}
@@ -72,6 +84,7 @@ def get_user_profile(access_token):
7284

7385
return response_json
7486

87+
7588
def get_recently_play(access_token):
7689

7790
headers = {"Authorization": f"Bearer {access_token}"}
@@ -84,6 +97,7 @@ def get_recently_play(access_token):
8497
response_json = response.json()
8598
return response_json
8699

100+
87101
def get_now_playing(access_token):
88102

89103
headers = {"Authorization": f"Bearer {access_token}"}

0 commit comments

Comments
 (0)