Skip to content

Commit 750dbe0

Browse files
committed
fix event loop blocking for sync routes and offload get_user_from_request
1 parent e9da8e3 commit 750dbe0

7 files changed

Lines changed: 168 additions & 56 deletions

File tree

routers/core/account.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def logout(
188188

189189

190190
@router.get("/login")
191-
async def read_login(
191+
def read_login(
192192
request: Request,
193193
_: None = Depends(require_unauthenticated_unless_invitation_warning),
194194
invitation_token: Optional[str] = Query(None),
@@ -215,7 +215,7 @@ async def read_login(
215215

216216

217217
@router.get("/register")
218-
async def read_register(
218+
def read_register(
219219
request: Request,
220220
_: None = Depends(require_unauthenticated_unless_invitation_warning),
221221
email: Optional[EmailStr] = Query(None),
@@ -246,7 +246,7 @@ async def read_register(
246246

247247

248248
@router.get("/forgot_password")
249-
async def read_forgot_password(
249+
def read_forgot_password(
250250
request: Request,
251251
_: None = Depends(require_unauthenticated_client),
252252
show_form: Optional[str] = "true",
@@ -262,7 +262,7 @@ async def read_forgot_password(
262262

263263

264264
@router.get("/reset_password")
265-
async def read_reset_password(
265+
def read_reset_password(
266266
request: Request,
267267
email: str,
268268
token: str,
@@ -291,7 +291,7 @@ async def read_reset_password(
291291

292292

293293
@router.post("/delete", response_class=RedirectResponse)
294-
async def delete_account(
294+
def delete_account(
295295
account: Account = Depends(get_verified_account),
296296
session: Session = Depends(get_session),
297297
):
@@ -314,7 +314,7 @@ async def delete_account(
314314

315315

316316
@router.post("/register", response_class=RedirectResponse)
317-
async def register(
317+
def register(
318318
request: Request,
319319
_ip_check: None = Depends(check_register_ip_rate_limit),
320320
name: str = Form(
@@ -473,7 +473,7 @@ async def register(
473473

474474

475475
@router.post("/login", response_class=RedirectResponse)
476-
async def login(
476+
def login(
477477
request: Request,
478478
_ip_check: None = Depends(check_login_ip_rate_limit),
479479
_email_check: EmailStr = Depends(check_login_email_rate_limit),
@@ -588,7 +588,7 @@ async def login(
588588

589589
# Updated refresh_token endpoint
590590
@router.post("/refresh", response_class=RedirectResponse)
591-
async def refresh_token(
591+
def refresh_token(
592592
tokens: tuple[Optional[str], Optional[str]] = Depends(oauth2_scheme_cookie),
593593
session: Session = Depends(get_session),
594594
) -> RedirectResponse:
@@ -666,7 +666,7 @@ async def refresh_token(
666666

667667

668668
@router.post("/forgot_password")
669-
async def forgot_password(
669+
def forgot_password(
670670
background_tasks: BackgroundTasks,
671671
request: Request,
672672
_ip_check: None = Depends(check_forgot_password_ip_rate_limit),
@@ -703,7 +703,7 @@ async def forgot_password(
703703

704704

705705
@router.post("/reset_password")
706-
async def reset_password(
706+
def reset_password(
707707
request: Request,
708708
email: EmailStr = Form(..., title="Email", description="Account email address"),
709709
token: str = Form(
@@ -765,7 +765,7 @@ async def reset_password(
765765

766766

767767
@router.get("/recover")
768-
async def recover_account_confirm(
768+
def recover_account_confirm(
769769
request: Request,
770770
token: str = Query(...),
771771
session: Session = Depends(get_session),
@@ -784,7 +784,7 @@ async def recover_account_confirm(
784784

785785

786786
@router.post("/recover")
787-
async def recover_account(
787+
def recover_account(
788788
token: str = Form(...),
789789
session: Session = Depends(get_session),
790790
):
@@ -852,7 +852,7 @@ async def recover_account(
852852

853853

854854
@router.post("/emails/add")
855-
async def add_email(
855+
def add_email(
856856
request: Request,
857857
new_email: EmailStr = Form(
858858
..., title="New email", description="New email address to add"
@@ -905,7 +905,7 @@ async def add_email(
905905

906906

907907
@router.get("/emails/verify")
908-
async def verify_email(
908+
def verify_email(
909909
token: str,
910910
session: Session = Depends(get_session),
911911
):
@@ -958,7 +958,7 @@ async def verify_email(
958958

959959

960960
@router.post("/emails/promote")
961-
async def promote_email(
961+
def promote_email(
962962
request: Request,
963963
email_id: int = Form(
964964
..., title="Email ID", description="ID of the email to promote"
@@ -1047,7 +1047,7 @@ async def promote_email(
10471047

10481048

10491049
@router.post("/emails/remove")
1050-
async def remove_email(
1050+
def remove_email(
10511051
request: Request,
10521052
email_id: int = Form(
10531053
..., title="Email ID", description="ID of the email to remove"

routers/core/dashboard.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616

1717
@router.get("/")
18-
async def read_dashboard(
18+
def read_dashboard(
1919
request: Request,
2020
user: User = Depends(get_user_with_relations),
2121
session: Session = Depends(get_session),
@@ -77,7 +77,7 @@ async def read_dashboard(
7777

7878

7979
@router.post("/select-organization/{org_id}")
80-
async def select_organization(
80+
def select_organization(
8181
request: Request,
8282
org_id: int,
8383
user: User = Depends(get_user_with_relations),

routers/core/invitation.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def _members_table_response(
108108

109109

110110
@router.post("/", name="create_invitation")
111-
async def create_invitation(
111+
def create_invitation(
112112
request: Request,
113113
current_user: User = Depends(get_authenticated_user),
114114
session: Session = Depends(get_session),
@@ -205,7 +205,7 @@ async def create_invitation(
205205

206206

207207
@router.post("/resend", name="resend_invitation", response_class=RedirectResponse)
208-
async def resend_invitation(
208+
def resend_invitation(
209209
request: Request,
210210
current_user: User = Depends(get_authenticated_user),
211211
session: Session = Depends(get_session),
@@ -277,7 +277,7 @@ async def resend_invitation(
277277

278278

279279
@router.post("/delete", name="delete_invitation", response_class=RedirectResponse)
280-
async def delete_invitation(
280+
def delete_invitation(
281281
request: Request,
282282
current_user: User = Depends(get_authenticated_user),
283283
session: Session = Depends(get_session),
@@ -318,7 +318,7 @@ async def delete_invitation(
318318

319319

320320
@router.get("/accept", name="accept_invitation")
321-
async def accept_invitation(
321+
def accept_invitation(
322322
token: str = Query(...),
323323
current_user: Optional[User] = Depends(get_optional_user),
324324
session: Session = Depends(get_session),

routers/core/organization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737

3838
@router.get("/{org_id}")
39-
async def read_organization(
39+
def read_organization(
4040
org_id: int,
4141
request: Request,
4242
user: User = Depends(get_user_with_relations),

routers/core/user.py

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fastapi import APIRouter, Depends, Form, UploadFile, File, Request, HTTPException
22
from fastapi.responses import RedirectResponse, Response
3+
from starlette.concurrency import run_in_threadpool
34
from sqlmodel import Session, select, col
45
from typing import Optional, List
56
from fastapi.templating import Jinja2Templates
@@ -51,7 +52,7 @@
5152

5253

5354
@router.get("/profile")
54-
async def read_profile(
55+
def read_profile(
5556
request: Request,
5657
user: User = Depends(get_user_with_relations),
5758
session: Session = Depends(get_session),
@@ -82,7 +83,7 @@ async def read_profile(
8283

8384

8485
@router.get("/edit-form")
85-
async def edit_profile_form(
86+
def edit_profile_form(
8687
request: Request,
8788
user: User = Depends(get_authenticated_user),
8889
):
@@ -104,7 +105,7 @@ async def edit_profile_form(
104105

105106

106107
@router.get("/profile-display")
107-
async def profile_display(
108+
def profile_display(
108109
request: Request,
109110
user: User = Depends(get_authenticated_user),
110111
):
@@ -130,8 +131,10 @@ async def update_profile(
130131
session: Session = Depends(get_session),
131132
):
132133
avatar_changed = bool(avatar_file and avatar_file.filename)
134+
avatar_data: Optional[bytes] = None
135+
avatar_content_type: Optional[str] = None
133136

134-
# Handle avatar update
137+
# Async chunked read must stay on the event loop; sync image/DB work is offloaded.
135138
if avatar_changed:
136139
assert avatar_file is not None
137140
reject_oversized_content_length(
@@ -140,25 +143,14 @@ async def update_profile(
140143
avatar_data = await read_upload_with_size_limit(avatar_file, MAX_FILE_SIZE)
141144
avatar_content_type = avatar_file.content_type
142145

143-
processed_image, content_type = validate_and_process_image(
144-
avatar_data, avatar_content_type
145-
)
146-
if user.avatar:
147-
user.avatar.avatar_data = processed_image
148-
user.avatar.avatar_content_type = content_type
149-
else:
150-
assert user.id is not None
151-
user.avatar = UserAvatar(
152-
user_id=user.id,
153-
avatar_data=processed_image,
154-
avatar_content_type=content_type,
155-
)
156-
157-
# Update user details
158-
user.name = name
159-
160-
session.commit()
161-
session.refresh(user)
146+
await run_in_threadpool(
147+
_apply_profile_update,
148+
session,
149+
user,
150+
name,
151+
avatar_data,
152+
avatar_content_type,
153+
)
162154

163155
if is_htmx_request(request):
164156
response = templates.TemplateResponse(
@@ -184,8 +176,36 @@ async def update_profile(
184176
return RedirectResponse(url=router.url_path_for("read_profile"), status_code=303)
185177

186178

179+
def _apply_profile_update(
180+
session: Session,
181+
user: User,
182+
name: Optional[str],
183+
avatar_data: Optional[bytes],
184+
avatar_content_type: Optional[str],
185+
) -> None:
186+
"""Sync image processing and DB persistence for update_profile."""
187+
if avatar_data is not None:
188+
processed_image, content_type = validate_and_process_image(
189+
avatar_data, avatar_content_type
190+
)
191+
if user.avatar:
192+
user.avatar.avatar_data = processed_image
193+
user.avatar.avatar_content_type = content_type
194+
else:
195+
assert user.id is not None
196+
user.avatar = UserAvatar(
197+
user_id=user.id,
198+
avatar_data=processed_image,
199+
avatar_content_type=content_type,
200+
)
201+
202+
user.name = name
203+
session.commit()
204+
session.refresh(user)
205+
206+
187207
@router.post("/communication-preferences", response_class=RedirectResponse)
188-
async def update_communication_preferences(
208+
def update_communication_preferences(
189209
request: Request,
190210
comm_opt_in: Optional[str] = Form(None),
191211
comm_updates: Optional[str] = Form(None),
@@ -210,7 +230,7 @@ async def update_communication_preferences(
210230

211231

212232
@router.get("/avatar")
213-
async def get_avatar(user: User = Depends(get_authenticated_user)):
233+
def get_avatar(user: User = Depends(get_authenticated_user)):
214234
"""Serve avatar image from database"""
215235
if not user.avatar:
216236
raise DataIntegrityError(resource="User avatar")

0 commit comments

Comments
 (0)