Joshua Steele
Joshua Steele

Reputation: 65

Django application running on top of Serverless + Lambda + API Gateway HTTP API is rewriting links to be prefixed with default

My Django Application (Largely REST Framework based) is currently producing URLs on the admin page that don't resolve. The expected result is that the Django Admin's login prompt submits the form with a POST to /admin/login. The resultant URL passed by as the form submission URL by Django is /$default/admin/login and that returns a 404 with the even more obtuse /$default/$default/admin/login/.

I'm presuming I have some sort of misconfiguration in either my Django configuration or serverless.yml.

As per the following serverless.yml I'm using API Gateway V2, Django through WSGI, and Lambda functions.

service: api
app: api
org: myapp
frameworkVersion: '3'
provider:
  name: aws
  runtime: python3.8
functions:
  serve:
    handler: wsgi_handler.handler
    timeout: 20
    environment:
      DB_NAME: ${param:db_name}
      DB_PASSWORD: ${param:db_password}
      DB_USER: ${param:db_user}
      DB_PORT: ${param:db_port}
      DB_HOST: ${param:db_host}
    events:
      - httpApi: "*"
  migration:
    handler: migrate.handler
    timeout: 60
    environment: 
      DB_NAME: ${param:db_name}
      DB_PASSWORD: ${param:db_password}
      DB_USER: ${param:db_user}
      DB_PORT: ${param:db_port}
      DB_HOST: ${param:db_host}
custom:
  wsgi:
    app: myapp.wsgi.application
plugins:
  - serverless-python-requirements
  - serverless-wsgi

My URLs are pretty standard:

from django.contrib import admin
from django.urls import path, include

from rest_framework.schemas import get_schema_view

schema_view = get_schema_view(
    title="MyApp",
    description="MyApp Universal API",
    version="1.0.0",
)

urlpatterns = [
    path("admin/", admin.site.urls),
    path("user/", include("myapp.core.urls"), name="user"),
    path("openapi", schema_view, name="openapi-schema"),
]

My configuration is even more standard:

import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "not for you :)"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']

if 'CODESPACE_NAME' in os.environ:
    codespace_name = os.getenv("CODESPACE_NAME")
    codespace_domain = os.getenv("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN")
    CSRF_TRUSTED_ORIGINS = [f'https://{codespace_name}-8000.{codespace_domain}']

ROOT_URLCONF = "myapp.urls"

# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    'rest_framework',
    "myapp.core",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

X_FRAME_OPTIONS = "ALLOW-FROM preview.app.github.dev"

ROOT_URLCONF = "myapp.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "myapp" / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "myapp.wsgi.application"


STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', 'neondb'),
        'USER': os.environ.get('DB_USER', 'postgres'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', 5432),
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

STATICFILES_DIRS = [
    BASE_DIR / "myapp" / "static",
]

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'

AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = "myapp-django-static"

AUTH_USER_MODEL = 'core.User'

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    )
}

As for the resultant error message:

enter image description here

Any help (or ideas) would be greatly appreciated!

I've tried modifying the path structure of the serverless.yml and have been trawling through the Django source code for any hints to no avail. Naturally I'd just like Django admin to work. As far as the rest of the app, it works fine as the API itself isn't self referential. Django just isn't returning the correct path.

To put it in brief, Django thinks that all of my routes are prefixed by /$default/ they are not. I'm looking for either a solution to force the path to sent by Django to be / or a fix for my Serverless configuration to mitigate this issue.

Upvotes: 1

Views: 288

Answers (1)

Joshua Steele
Joshua Steele

Reputation: 65

I managed to resolve this with a rather niché Django settings option: FORCE_SCRIPT_NAME

While this doesn't explain why it is resolving the path to /$default/ it does mitigate this issue.

If you're using this for a different use case than mine and your path is in a subdirectory (e.g the opposite issue to mine) then you would add your path in FORCE_SCRIPT_NAME instead.

See the link I've provided for more information.

Add the following to your app's settings.py, or any file you're using as your DJANGO_SETTINGS_MODULE.

# Force Django to resolve the URL to the root of the site
# This is required for API Gateway <- WSGI -> Django path resolution to work properly.
FORCE_SCRIPT_NAME = ""

Upvotes: 3

Related Questions