From 2d4712824218faa96d9da7d7772f78f2872f25db Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 12 Oct 2026 19:11:22 +0400 Subject: [PATCH] refac --- backend/open_webui/routers/apps.py | 44 ++++++++++------ backend/open_webui/utils/apps.py | 81 +++++++++++++++++++++--------- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/backend/open_webui/routers/apps.py b/backend/open_webui/routers/apps.py index 4382ead1a0..a5bfcfcbff 100644 --- a/backend/open_webui/routers/apps.py +++ b/backend/open_webui/routers/apps.py @@ -4,6 +4,7 @@ import time import uuid from urllib.parse import urlsplit +import jwt from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status from open_webui.internal.db import get_async_session from open_webui.models.access_grants import AccessGrant, AccessGrants @@ -81,12 +82,15 @@ async def preview_app_manifest(form_data: AppManifestForm, user=Depends(get_admi try: status_code, raw = await send_app_request(form_data.url) if status_code != 200: - raise ValueError() + raise HTTPException(502, f'App manifest endpoint returned HTTP {status_code}. Check the URL or enter details manually.') manifest = Manifest.model_validate(raw) return manifest.model_dump() - except Exception: + except HTTPException: + raise + except (ValueError, TypeError): raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid or unreachable app manifest' + status_code=status.HTTP_400_BAD_REQUEST, + detail='App returned an invalid manifest. Check its app details or enter them manually.', ) from None @@ -391,22 +395,32 @@ async def connect_app_by_id( ) await db.commit() challenge = str(uuid.uuid4()) + status_code, value = await send_app_event( + app, + {'id': str(uuid.uuid4()), 'type': 'connect', 'data': {'challenge': challenge}}, + timeout=20, + ) + if status_code != 200: + detail = { + 401: 'App rejected the API key. Enter the API key configured on the app server.', + 403: 'App refused the connection. Check the app server access rules and logs.', + 400: 'App rejected the connection request. Check the WebUI origin and app server configuration.', + 404: 'App event endpoint was not found. Check the URL in app settings.', + 405: 'App endpoint does not accept POST requests. Use its event endpoint URL.', + 503: 'App is busy or shutting down. Connect again when it is ready.', + 504: 'App connection request timed out. Check the app server logs.', + }.get(status_code, f'App returned HTTP {status_code}. Check the app server logs.') + raise HTTPException(504 if status_code == 504 else 502, detail) + if not isinstance(value, dict) or not isinstance(value.get('proof'), str): + raise HTTPException(502, 'App did not return an identity proof. Check its event endpoint and SDK version.') try: - status_code, value = await send_app_event( - app, - {'id': str(uuid.uuid4()), 'type': 'connect', 'data': {'challenge': challenge}}, - timeout=20, - ) - if status_code != 200 or not isinstance(value, dict): - raise ValueError() claims = verify_app_token(app, value['proof'], 'webui-app-connect+jwt') if claims.get('challenge') != challenge: raise ValueError() - except Exception: - raise HTTPException( - status_code=status.HTTP_502_BAD_GATEWAY, - detail='Could not connect. Check the app API key and installed public key.', - ) from None + except jwt.InvalidSignatureError: + raise HTTPException(502, 'App public key does not match the installed key. Review the app public key in settings.') from None + except (jwt.InvalidTokenError, ValueError, TypeError, KeyError): + raise HTTPException(502, 'App identity proof is invalid or expired. Check the installed public key and server clocks.') from None result = await db.execute( update(App) .where(App.id == app_id, App.updated_at == checked_at, App.api_key == api_key) diff --git a/backend/open_webui/utils/apps.py b/backend/open_webui/utils/apps.py index 2fa159baaa..d6b3b9cfa1 100644 --- a/backend/open_webui/utils/apps.py +++ b/backend/open_webui/utils/apps.py @@ -92,27 +92,47 @@ async def send_app_request( timeout: float = 10, max_response_size: int = 262144, ) -> tuple[int, Any]: - allow_private = validate_app_url(url) - async with aiohttp.ClientSession( - connector=AppTCPConnector(allow_private=allow_private), - timeout=aiohttp.ClientTimeout(total=timeout), - trust_env=False, - ) as session: - async with session.request( - 'POST' if body is not None else 'GET', - url, - data=body, - headers=headers, - allow_redirects=False, - ) as response: - response_body = bytearray() - async for chunk in response.content.iter_chunked(16384): - response_body.extend(chunk) - if len(response_body) > max_response_size: - raise ValueError('App response is too large') - if response.status != 200: - return response.status, None - return response.status, json.loads(response_body) if response_body else None + try: + allow_private = validate_app_url(url) + async with aiohttp.ClientSession( + connector=AppTCPConnector(allow_private=allow_private), + timeout=aiohttp.ClientTimeout(total=timeout), + trust_env=False, + ) as session: + async with session.request( + 'POST' if body is not None else 'GET', + url, + data=body, + headers=headers, + allow_redirects=False, + ) as response: + response_body = bytearray() + async for chunk in response.content.iter_chunked(16384): + response_body.extend(chunk) + if len(response_body) > max_response_size: + raise ValueError('App response is too large') + if response.status != 200: + return response.status, None + return response.status, json.loads(response_body) if response_body else None + except TimeoutError: + raise HTTPException(504, 'App request timed out. Check that the app server is running and reachable.') from None + except aiohttp.ClientSSLError: + raise HTTPException(502, 'App TLS verification failed. Check its certificate and hostname.') from None + except aiohttp.ClientConnectorDNSError: + raise HTTPException(502, 'App hostname could not be resolved. Check DNS from the WebUI server.') from None + except aiohttp.ClientConnectorError: + raise HTTPException(502, 'Could not reach the app server. Check its address, port and network access.') from None + except aiohttp.ClientError: + raise HTTPException(502, 'The app connection failed before a complete response was received.') from None + except ValueError as exc: + # Only local, fixed diagnostics may be shown; never echo arbitrary app responses. + details = { + 'Invalid service URL': 'Invalid app URL. Use an HTTP(S) URL without credentials or a fragment.', + 'App endpoints require HTTPS': 'App endpoints require HTTPS. For local HTTP, configure APP_ALLOWED_PRIVATE_ORIGINS.', + 'App endpoint resolves to a prohibited address': 'App resolves to a private or reserved address. For local development, configure APP_ALLOWED_PRIVATE_ORIGINS.', + 'App response is too large': 'App response exceeds the allowed size.', + } + raise HTTPException(502, details.get(str(exc), 'App returned an invalid response. Expected JSON.')) from None def get_app_encryption_cipher() -> Fernet: @@ -490,10 +510,23 @@ async def generate_app_channel_reply( await db.commit() try: status, raw = await send_app_event(app, envelope) - except Exception: - raise HTTPException(504, 'App response timed out; execution outcome may be unknown') from None + except HTTPException as exc: + if exc.status_code == 504: + raise HTTPException(504, 'App response timed out; execution outcome may be unknown') from None + raise + except ValueError: + raise HTTPException(502, 'App event could not be delivered. Check the WebUI server logs.') from None if status != 200: - raise HTTPException(502, 'App rejected the interaction') + detail = { + 401: 'App rejected the API key. Update the key in app settings.', + 403: 'App rejected event authorization. Check the app server logs for the verification failure.', + 404: 'App event endpoint was not found. Check its URL in app settings.', + 405: 'App endpoint does not accept POST requests. Check its event URL.', + 413: 'App rejected the event because it is too large.', + 503: 'App is busy or shutting down. The event was not accepted.', + 504: 'App processing timed out; execution outcome may be unknown.', + }.get(status, f'App returned HTTP {status}. Check the app server logs.') + raise HTTPException(504 if status == 504 else 502, detail) app = await get_app_by_id(app.id, require_active=True, db=db) if (app.api_key, app.data['manifest']['public_key']) != checked_key: raise HTTPException(403, 'App credentials changed')