mawoka-myblock / ClassQuiz

ClassQuiz is a quiz-application like Kahoot!, but open-source.
https://classquiz.de
Mozilla Public License 2.0
440 stars 80 forks source link

File Upload Failure: HTTP 500 Error on /api/v1/storage Endpoint with Uppy Integration #405

Open Mergim1992 opened 12 hours ago

Mergim1992 commented 12 hours ago

Which component is affected?

Media-Upload (.png)

Did the issue occur at ClassQuiz.de, or on a self-hosted instance?

On a self-hosted instance

How can the issue be reproduced?

Deploy the application using the provided docker-compose.yml and Nginx configuration. Attempt to upload a file using the Uppy interface. Observe the 500 Internal Server Error in the browser console logs.

docker-compose.yml:

# SPDX-FileCopyrightText: 2023 Marlon W (Mawoka)
#
# SPDX-License-Identifier: MPL-2.0

version: "3"

services:
  frontend:
    restart: always
    build:
      context: ./frontend
      dockerfile: Dockerfile
    depends_on:
      - redis
      - api
    environment:
      REDIS_URL: redis://redis:6379/0?decode_responses=True
      API_URL: http://api:80
  api:
    build: &build_cfg
      context: .
      dockerfile: Dockerfile
    restart: &restart always
    depends_on: &depends
      - db
      - redis

    environment: &env_vars
      # --- DON'T CHANGE FROM HERE ---
      DB_URL: "postgresql://postgres:classquiz@db:5432/classquiz" # DON'T CHANGE
      REDIS: "redis://redis:6379/0?decode_responses=True" # DON'T CHANGE
      SECRET_KEY: "10f3c7c73bdc92a8507992197c4ab73c480953a1892e79006304fadece2caf36" # Don't change it manually, use the one-liner provided in the documentation
      MAX_WORKERS: "1" # Very important and DON'T CHANGE
      ACCESS_TOKEN_EXPIRE_MINUTES: 30  # DON'T CHANGE
      MEILISEARCH_URL: "http://meilisearch:7700" # DON'T CHANGE
      # -- DON'T CHANGE TILL HERE ---

      # --- GENERAL CONFI ---
      ROOT_ADDRESS: "http://quiz.mysite.de" # CHANGE IT (without a "/" at the end)

      # --- MAIL CONFIG ---
      MAIL_PORT: "587"
      MAIL_ADDRESS: "email@email@email.email"
      MAIL_PASSWORD: "PASSWORT"
      MAIL_USERNAME: "email@email@email.email"
      MAIL_SERVER: "email@email@email.emai"
      SKIP_EMAIL_VERIFICATION: "True" # Does the user have to confirm its email by clicking a link?

      # --- EXTERNAL API CONFIG ---
      # HCAPTCHA_KEY: "HCAPTCHA_PRIVATE_KEY"
      # PIXABAY_API_KEY: "" # Get it from here: https://pixabay.com/api/docs/
      # RECAPTCHA_KEY: "" Get it from Google for the Captcha.

      # -- STORAGE CONFIG ---
      STORAGE_BACKEND: "local" # Could also be s3
      STORAGE_PATH: "/var/www/mysite/classquiz/uploads" # When s3 is used, this isn't needed.
      # If STORAGE_BACKEND is "s3"
      #S3_ACCESS_KEY: "YOUR_ACCESS_KEY"
      #S3_SECRET_KEY: "YOUR_SECRET_KEY"
      #S3_BASE_URL: "YOUR_S3_BASE_URL"

      # --- GOOGLE_AUTH ---
      #GOOGLE_CLIENT_ID: "" # Your Google-Client ID, or leave it unset if you don't want it.
      #GOOGLE_CLIENT_SECRET: "" # Your Google-Client Secret, or leave it unset if you don't want it.

      # --- GITHUB_AUTH ---
      #GITHUB_CLIENT_ID: "" # Your GitHub-Client ID, or leave it unset if you don't want it.
      #GITHUB_CLIENT_SECRET: "" # Your GitHub-Client Secret, or leave it unset if you don't want it.

      # --- Custom OpenID ---
      #CUSTOM_OPENID_PROVIDER__CLIENT_ID: "" # Adjust if needed
      #CUSTOM_OPENID_PROVIDER__CLIENT_SECRET: "" # Adjust if needed
      #CUSTOM_OPENID_PROVIDER__SERVER_METADATA_URL: "/.well-known/openid-configuration" # Adjust if needed

    volumes: # Only needed if you chose the "local" storage-backend
      - /var/www/mysite/classquiz/uploads:/var/storage

  redis:
    image: redis:alpine
    restart: always
    healthcheck:
      test: [ "CMD", "redis-cli","ping" ]

  db:
    image: postgres:14-alpine
    restart: always
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U postgres" ]
      interval: 5s
      timeout: 5s
      retries: 5
    environment:
      POSTGRES_PASSWORD: "classquiz"
      POSTGRES_DB: "classquiz"
    volumes:
      - data:/var/lib/postgresql/data
  proxy:
    image: caddy:alpine
    restart: always
    volumes:
      - ./Caddyfile-docker:/etc/caddy/Caddyfile
    ports:
      - "8000:8080" # The 8000 can be changed.
  meilisearch:
    image: getmeili/meilisearch:v0.28.0
    restart: always
    environment:
      MEILI_NO_ANALYTICS: "true"
    volumes:
      - meilisearch-data:/data.ms
  worker:
    build: *build_cfg
    environment: *env_vars
    depends_on: *depends
    restart: *restart
    command: arq classquiz.worker.WorkerSettings

volumes:
  data:
  meilisearch-data:

quiz.mysite.de (nginx config):

# HTTP-Konfiguration für quiz.mysite.de
server {
    listen 80;
    listen [::]:80;
    server_name quiz.mysite.de;

    # Weiterleitung von HTTP auf HTTPS
    # If you plan to use SSL, enable the following line.
    return 301 https://$host$request_uri;

    # Proxy pass to your application on port 8000
    #location / {
    #    proxy_pass http://127.0.0.1:8000;  # Forward requests to port 8000
    #    proxy_set_header X-Real-IP $remote_addr;
    #    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    #    proxy_set_header Host $host;
    #    proxy_set_header X-Forwarded-Proto $scheme;
    #    proxy_redirect off;
    #}
}

# HTTPS-Konfiguration für quiz.mysite.de
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name quiz.mysite.de www.quiz.mysite.de;

    # Zertifikats-Dateien von Let's Encrypt
    ssl_certificate /etc/letsencrypt/live/quiz.mysite.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/quiz.mysite.de/privkey.pem;

    # Proxy pass to your application on port 8000
    location / {
        proxy_pass http://127.0.0.1:8000;  # Forward requests to port 8000
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;

        # CORS-Header hinzufügen
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
    }

    location ~ /\.ht {
        deny all;
    }
}

Describe the bug (with screenshots if possible)

While attempting to upload files via the /api/v1/storage endpoint using Uppy in a Dockerized environment, the server returns a 500 Internal Server Error. The backend is running in a containerized setup with Nginx as the proxy, and file storage is configured using the local storage backend. The issue persists despite adding necessary CORS headers in the Nginx configuration.

This is the web-console error I receive when upload a picture:


XHRPOST
https://quiz.mysite/api/v1/storage/
[HTTP/2 500  630ms]

XHRPOST
https://quiz.mysite/api/v1/storage/
[HTTP/2 500  711ms]

XHRPOST
https://quiz.mysite/api/v1/storage/
[HTTP/2 500  608ms]

XHRPOST
https://quiz.mysite/api/v1/storage/
[HTTP/2 500  577ms]

POST
    https://quiz.mysite/api/v1/storage/
Status
500
VersionHTTP/2
Übertragen185 B (21 B Größe)
Referrer Policystrict-origin-when-cross-origin
DNS-AuflösungSystem

[Uppy] [17:11:51] Upload error loggers.js:13:19
    error loggers.js:13
    log Uppy.js:1128
    vh Uppy.js:1476
    r index.js:131
    emit index.js:33
    emit Uppy.js:312
    <anonym> index.js:230
    i index.js:348
    o RateLimitedQueue.js:148
    tp RateLimitedQueue.js:231
    run RateLimitedQueue.js:113
    l RateLimitedQueue.js:144
    wrapPromiseFunction RateLimitedQueue.js:143
    gp index.js:364
    bp index.js:447
    bp index.js:428
    value index.js:149
    _h Uppy.js:1726
    upload Uppy.js:1240
    (Async: promise callback)
    upload Uppy.js:1225
    startUpload StatusBar.js:99
    Preact 27
    <anonym> UIPlugin.js:101
    e UIPlugin.js:27
    (Async: promise callback)
    Ah UIPlugin.js:21
    update UIPlugin.js:151
XHRPOST
https://quiz.mysite/api/v1/storage/
[HTTP/2 500  1805ms]

XHRPOST
https://quiz.mysite/api/v1/storage/
[HTTP/2 500  1248ms]

XHRPOST
https://quiz.mysite/api/v1/storage/
[HTTP/2 500  635ms]

XHRPOST
https://quiz.mysite/api/v1/storage/
[HTTP/2 500  580ms]

[Uppy] [17:14:59] Upload error loggers.js:13:19

Device

Desktop

Operating System

Linux Ubuntu 24.04

Browser

Safari, Firefox

mawoka-myblock commented 12 hours ago

Please also share the logs from the api docker container

Mergim1992 commented 11 hours ago

Hi @mawoka-myblock ,

thanks for the fast response !

My docker container logs for the api:

root@ubuntu:/var/www/mysite/classquiz# docker logs --tail 100 classquiz_api_1
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/classquiz/__init__.py", line 78, in auth_middleware_wrapper
    return await rememberme_middleware(request, call_next)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/classquiz/oauth/__init__.py", line 79, in rememberme_middleware
    response: Response = await call_next(request)
                         ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 165, in call_next
    raise app_exc
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 151, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 189, in __call__
    with collapse_excgroups():
  File "/usr/local/lib/python3.11/contextlib.py", line 158, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/usr/local/lib/python3.11/site-packages/starlette/_utils.py", line 93, in collapse_excgroups
    raise exc
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 191, in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/classquiz/__init__.py", line 57, in sentry_exception
    raise e
  File "/app/classquiz/__init__.py", line 51, in sentry_exception
    response = await call_next(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 165, in call_next
    raise app_exc
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/base.py", line 151, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File "/usr/local/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/usr/local/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 756, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 776, in app
    await route.handle(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 297, in handle
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 77, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "/usr/local/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/usr/local/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/usr/local/lib/python3.11/site-packages/starlette/routing.py", line 72, in app
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/fastapi/routing.py", line 278, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/fastapi/routing.py", line 191, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/classquiz/routers/storage.py", line 138, in upload_file
    await storage.upload(
  File "/app/classquiz/storage/__init__.py", line 52, in upload
    return await self.instance.upload(file=file_data, file_name=file_name, mime_type=mime_type, size=size)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/classquiz/storage/local_storage.py", line 39, in upload
    with open(file=os.path.join(self.base_path, file_name), mode="wb") as f:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/var/www/mysite/classquiz/uploads/e435ebe37fb743f190700205fcfdc09d'
172.27.0.5:59994 - "POST /api/v1/storage/ HTTP/1.1" 401
172.27.0.5:37808 - "POST /api/v1/login HTTP/1.1" 404
172.27.0.5:36112 - "POST /api/v1/auth/login HTTP/1.1" 404
172.27.0.5:35650 - "GET /api/v1/users/logout HTTP/1.1" 302
172.27.0.5:35930 - "POST /api/v1/login/start HTTP/1.1" 200
172.27.0.5:48660 - "POST /api/v1/login/step/1?session_id=cf8489c403134547ff5c9c0ca529f4be HTTP/1.1" 200
172.27.0.5:48660 - "POST /api/v1/editor/start?edit=false HTTP/1.1" 200
172.27.0.5:52586 - "POST /api/v1/login/start HTTP/1.1" 200
172.27.0.5:45850 - "POST /api/v1/editor/start?edit=false HTTP/1.1" 200
172.27.0.5:49366 - "GET /api/v1/quiz/list?page_size=100 HTTP/1.1" 200
172.27.0.5:49366 - "GET /api/v1/quiztivity/ HTTP/1.1" 200
172.27.0.5:36520 - "POST /api/v1/storage/ HTTP/1.1" 401
172.27.0.5:33946 - "POST /api/v1/storage/ HTTP/1.1" 401
172.27.0.5:56714 - "GET /api/v1/users/logout HTTP/1.1" 302
172.27.0.5:40766 - "POST /api/v1/login/start HTTP/1.1" 200
172.27.0.5:40766 - "POST /api/v1/login/step/1?session_id=eae8bb852b34fda6d4a61efc5ce9a1d0 HTTP/1.1" 200
172.27.0.5:40766 - "GET /api/v1/quiz/list?page_size=100 HTTP/1.1" 200
172.27.0.5:40766 - "GET /api/v1/quiztivity/ HTTP/1.1" 200
172.27.0.5:40766 - "GET /api/v1/quiz/get/d998c132-f780-4383-8dc6-a5f12d04ed4f HTTP/1.1" 200
172.27.0.5:40766 - "GET /api/v1/quiz/get/d998c132-f780-4383-8dc6-a5f12d04ed4f HTTP/1.1" 200
172.27.0.5:40766 - "POST /api/v1/editor/start?edit=true&quiz_id=d998c132-f780-4383-8dc6-a5f12d04ed4f HTTP/1.1" 200
172.27.0.5:42516 - "GET /api/v1/quiz/list?page_size=100 HTTP/1.1" 200
172.27.0.5:42516 - "GET /api/v1/quiztivity/ HTTP/1.1" 200
172.27.0.5:54814 - "POST /api/v1/storage/ HTTP/1.1" 401
172.27.0.5:54814 - "POST /api/v1/auth/refresh-token HTTP/1.1" 404
172.27.0.5:53980 - "POST /api/v1/storage/ HTTP/1.1" 401
172.27.0.5:58504 - "POST /api/v1/storage/ HTTP/1.1" 401
172.27.0.5:52642 - "GET /api/v1/users/logout HTTP/1.1" 302
172.27.0.5:45178 - "POST /api/v1/login/start HTTP/1.1" 200
172.27.0.5:47690 - "POST /api/v1/login/start HTTP/1.1" 200
172.27.0.5:47690 - "POST /api/v1/login/step/1?session_id=8f10e6a1eb2bf6eafb077b4be4660c0f HTTP/1.1" 200
172.27.0.5:47690 - "POST /api/v1/editor/start?edit=false HTTP/1.1" 200
172.27.0.5:57348 - "POST /api/v1/storage/ HTTP/1.1" 401
root@ubuntu:/var/www/mysite/classquiz# 

meanwhile I also added a user in docker-compose.yml:

  api:
    build: &build_cfg
      context: .
      dockerfile: Dockerfile
    restart: &restart always
    depends_on: &depends
      - db
      - redis
    **user: www-data**
    environment: &env_vars
      # --- DON'T CHANGE FROM HERE ---

Kind regards :-)

mawoka-myblock commented 11 hours ago

FileNotFoundError: [Errno 2] No such file or directory: '/var/www/mysite/classquiz/uploads/e435ebe37fb743f190700205fcfdc09d'

The directory /var/www/mysite/classquiz/uploads/ doesn't exist. Check your docker compose mounts