Search code examples
javascriptdjangoapachedeployment

Javascript fetch return 403 on Apache Server


Struggling with this issue for a few days now so any help is greatly appreciated.

I managed to deploy my django project on a Linux server and it works fine apart from the model form submit. On the local development server it work fine and here is the workflow:

  1. User fills the form;
  2. User clicks submit;
  3. Javascript catches the submit event, submits a "POST" request and gets a response back;
  4. On the server side, the form is checked and if all is good an email is sent;
  5. HTML is updated to add the confirmation of form registration;

Here is my code:

home.html

    <div class="col-lg-8 offset-lg-2">
    <form method="POST" class="row mt-17" id="form" onsubmit="return false">
        {% csrf_token %}
        {% for field in form %}
        {% if field.name not in "message" %}
        <div class="col-12 col-sm-6">
            <div class=form-group>
                <label for={{field.name}} class="form-label">{{ field.label }}</label>
                {{ field }}
            </div>
        </div>
        {% else %}
        <div class="col-12">
            <div class=form-group>
                <label for={{field.name}} class="form-label">{{ field.label }}</label>
                {{ field }}
            </div>
        </div>
        {% endif %}
        {% endfor %}
        <div class="form-group mb-0">
            <button type="submit" class="btn btn-primary btn-lg">Submit</button>
        </div>
    </form>
</div>

main.js

const form = document.getElementById('form');
form.addEventListener("submit", submitHandler);
function submitHandler(e) {
    e.preventDefault();
    fetch("{% url 'messages-api' %}", {
        credentials: "include",
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new FormData(form)
    })
    .then(response => response.json())
    .then(data => {
        alert("Got your message")
        })

}

ssl.conf file

    <Directory /home/admin/pinpoint/templates>
            Require all granted
    </Directory>

    Alias /static /home/admin/pinpoint/static
    <Directory /home/admin/pinpoint/static>
            Require all granted
    </Directory>

    Alias /media /home/admin/pinpoint/media
    <Directory /home/admin/pinpoint/media>
            Require all granted
    </Directory>

    <Directory /home/admin/pinpoint/project>
            <Files wsgi.py>
                    Require all granted
            </Files>
    </Directory>

    WSGIScriptAlias / /home/admin/pinpoint/project/wsgi.py
    WSGIDaemonProcess pinpoint python-path=/home/admin/pinpoint python-home=/home/admin/pinpoint/venv
    WSGIProcessGroup pinpoint

On the development server, whenever you click submit, I get 403 (Forbidden) in the console.

Since the development version works fine, my guess would be it's a permission issue. As such, I gave apache ownership and r+w rights in my templates folder. Still the issue persists.

If i remove the headers content in the fetch command, the form is registered in the database but after ~2 minutes I get Server Error(500).

Any help/suggestions are welcome.


Solution

  • The issue is likely related to CSRF protection and a possible improper handling of the form data. I made some changes and added additional steps to help resolve the problem as seen below:

    // --- Updated form submission code ---
    
    // main.js
    const form = document.getElementById('form');
    form.addEventListener("submit", submitHandler);
    
    async function submitHandler(e) {
        e.preventDefault();
        
        // Get the CSRF token from the cookie
        const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
        
        const formData = new FormData(form);
        
        try {
            const response = await fetch("{% url 'messages-api' %}", {
                method: 'POST',
                credentials: 'same-origin',  // Important for CSRF
                headers: {
                    'X-CSRFToken': csrftoken,
                    // Don't set Content-Type when using FormData
                },
                body: formData
            });
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            const data = await response.json();
            alert("Got your message");
            
        } catch (error) {
            console.error('Error:', error);
            alert("There was an error submitting the form. Please try again.");
        }
    }
    

    The 403 Forbidden error occurs when Django’s CSRF protection blocks the request so I updated the code to properly include the CSRF token in the request headers. Also, using credentials: ‘same-origin’ ensures cookies are sent with the request

    ‘Content-Type’ header was removed because when using FormData, the browser needs to set this automatically. This allows the browser to properly set the boundary for multipart form data.

    Add these lines to your Apache configuration (ssl.conf) to handle CORS and CSRF:

    # Add to your ssl.conf
    Header set Access-Control-Allow-Origin "*"
    Header set Access-Control-Allow-Headers "Content-Type, X-CSRFToken"
    Header set Access-Control-Allow-Methods "POST, GET, OPTIONS"
    

    See to it that these Django settings are properly configured in your settings.py:

    CSRF_COOKIE_SECURE = True  # for HTTPS
    CSRF_COOKIE_HTTPONLY = False  # Allows JavaScript to read the cookie
    CSRF_TRUSTED_ORIGINS = ['https://yourdomain.com']  # Add your domain
    

    Check these permissions on your server:

    # Set appropriate permissions for Apache
    sudo chown -R www-data:www-data /home/admin/pinpoint
    sudo chmod -R 755 /home/admin/pinpoint
    
    # Make sure media and static directories are writable
    sudo chmod -R 775 /home/admin/pinpoint/media
    sudo chmod -R 775 /home/admin/pinpoint/static
    

    To figure out why you’re getting a 500 error after 2 minutes:

    Check your Apache error logs: /var/log/apache2/error.log

    Check your Django logs

    Temporarily set DEBUG = True in settings.py to see detailed error messages

    Add proper error handling in your Django view:

    # views.py
    from django.http import JsonResponse
    from django.views.decorators.csrf import ensure_csrf_cookie
    
    @ensure_csrf_cookie
    def your_view(request):
        try:
            # Your existing view logic here
            return JsonResponse({'status': 'success'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
    

    You may additionally check:

    • The browser’s developer tools Network tab to see the exact request/response

    • Look at the Apache and Django logs for specific error messages

    • Confirm if there are any timeouts configured in your server that might be causing the 2 minute delay

    I hope this helps!

    UPDATE: TO ALLOW FOR EMAILS TO BE SENT

    Email sending timeouts are a common issue in production deployments and are likely caused by the email sending blocking the main request thread. Here’s how this can be fixed:

    # Option 1: Using Django's send_mail_task
    from django.core.mail import send_mail
    from django.http import JsonResponse
    from django.views.decorators.csrf import ensure_csrf_cookie
    from asgiref.sync import async_to_sync
    import asyncio
    
    @ensure_csrf_cookie
    async def form_submission_view(request):
        try:
            # Your form processing logic here
            
            # Async email sending
            asyncio.create_task(send_email_async(
                subject="Your Subject",
                message="Your Message",
                from_email="[email protected]",
                recipient_list=["[email protected]"]
            ))
            
            return JsonResponse({'status': 'success'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
    
    async def send_email_async(subject, message, from_email, recipient_list):
        # Convert sync send_mail to async
        await asyncio.to_thread(send_mail,
            subject=subject,
            message=message,
            from_email=from_email,
            recipient_list=recipient_list,
            fail_silently=False,
        )
    
    # Option 2: Using Celery for background tasks
    from celery import shared_task
    from django.core.mail import send_mail
    
    @shared_task
    def send_email_task(subject, message, from_email, recipient_list):
        send_mail(
            subject=subject,
            message=message,
            from_email=from_email,
            recipient_list=recipient_list,
            fail_silently=False,
        )
    
    @ensure_csrf_cookie
    def form_submission_view_celery(request):
        try:
            # Your form processing logic here
            
            # Queue email task
            send_email_task.delay(
                subject="Your Subject",
                message="Your Message",
                from_email="[email protected]",
                recipient_list=["[email protected]"]
            )
            
            return JsonResponse({'status': 'success'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
    
    # settings.py additions for Celery
    CELERY_BROKER_URL = 'redis://localhost:6379/0'
    CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
    CELERY_ACCEPT_CONTENT = ['json']
    CELERY_TASK_SERIALIZER = 'json'
    CELERY_RESULT_SERIALIZER = 'json'
    CELERY_TIMEZONE = 'UTC'
    
    # Email settings
    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
    EMAIL_HOST = 'your.smtp.server'
    EMAIL_PORT = 587
    EMAIL_USE_TLS = True
    EMAIL_HOST_USER = '[email protected]'
    EMAIL_HOST_PASSWORD = 'your-email-password'
    DEFAULT_FROM_EMAIL = '[email protected]'
    

    I’ve provided 2 main solutions to handle email sending in production:

    1. Asynchronous View with asyncio (Option 1):
    • Uses Python's asyncio to handle email sending in the background
    • Simpler to implement but still runs in the same process
    • Good for smaller applications with moderate email volume
    • Requires running Django with an ASGI server like Daphne or Uvicorn
    1. Celery Task Queue (Option 2 - Recommended for production):
    • Handles email sending in a separate worker process
    • More robust and scalable solution
    • Better for handling multiple emails and preventing timeouts
    • Requires additional setup but is the standard solution for production

    To implement the Celery solution, you'll need to:

    Install required packages:

    pip install celery redis
    

    Create a celery.py file in your Django project:

    from __future__ import absolute_import, unicode_literals
    import os
    from celery import Celery
    
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings')
    
    app = Celery('your_project')
    app.config_from_object('django.conf:settings', namespace='CELERY')
    app.autodiscover_tasks()
    

    Install and start Redis (if not already installed):

    sudo apt-get install redis-server
    sudo systemctl start redis
    

    Start the Celery worker:

    celery -A your_project worker -l info
    

    Configure your Apache/WSGI setup to work with Celery:

    # Add to your Apache configuration
    WSGIDaemonProcess yourproject python-path=/path/to/virtualenv processes=2 threads=15
    WSGIProcessGroup yourproject
    

    Note: the email service (SMTP server) you’re using might affect the configuration needed.