Django mod-wsgi return empty response

I am trying to deploy a project on windows server with Apache24 on a Windows server. I am fairly new to this but I have spent more than a week trying to figure out what I need to do and what configurations I need to put in. The server is configured to work on HTTPS with a certificate. You can assume that any request coming on 80 or 8080 port is redirected to 443 that all works fine.

The VM config:

<VirtualHost *:443>
    DocumentRoot "${DOCROOT}/TIPS"
    ServerName   domain
    SSLEngine on
    SSLCertificateFile      "${SRVROOT}/conf/${SSLCRT}"
    SSLCertificateKeyFile   "${SRVROOT}/conf/${SSLPEM}"
    SSLCertificateChainFile "${SRVROOT}/conf/${SSLINTCRT}"
    #SSLCACertificateFile    "${SRVROOT}/conf/${SSLROOTCRT}"
	
	<Directory "${DOCROOT}/TIPS">
		Require all granted
	</Directory>
	
    <FilesMatch "\.(cgi|shtml|phtml|php|py)$">
        SSLOptions +StdEnvVars
    </FilesMatch>
	
    <Directory "${SRVROOT}/cgi-bin">
        SSLOptions +StdEnvVars
    </Directory>
	
	WSGIScriptAlias / "path/to/wsgi.py"
	# WSGIScriptAlias / "E:/wwwroot/TIPS/test.wsgi"

	

	# Alias for static files
	Alias /static "path/to/static"
	Alias /media "path/to/media"

	<Directory "path/to/static">
		Require all granted
	</Directory>
	
	<Directory "path/to/media">
		Require all granted
	</Directory>
	
	ErrorLog ${SRVROOT}/logs/error-TIPS.log
	CustomLog ${SRVROOT}/logs/access-TIPS.log combined

    BrowserMatch "MSIE [2-5]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0
</VirtualHost>

IN the httpd.conf file there are the following relevant configurations aside from some others:

RequestHeader unset Proxy early

TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz

LoadFile "C:/Program Files/Python310/python310.dll"
LoadModule wsgi_module "path/to/venv/lib/site-packages/mod_wsgi/server/mod_wsgi.cp310-win_amd64.pyd"
WSGIPythonHome "path/to/venv"
WSGIPythonPath "path/to/"

The weird thing is that if i request anything from the server apart from the django python file, i.e. images, it serves them alright. As soon as i request a URL that is mapped to the django project then I get empty response. I even replaced my index view function to a simple hello world. What is more weird is that I have a custom middleware and any print statement I put in there it gets printed as it should [msgi:error] but then the request sort of vanishes and therefore returns empty response error. Anyone has any idea as to why?

Thank you in advance.

Welcome @Eilleen !

Are you working from a tutorial or blog post for setting this up? It may be helpful to us if you identified what sources you’re using for references.

Have you checked these log files along with the general Apache log files for error messages? If there’s a fundamental error, it should be logged in one of them. Failing that, there might also be errors logged in the Windows system logs, you could check there, too.

Can you please post the actual contents of these files with the settings? Trying to anonymize or generalize them does not help when trying to address a specific situation.

Please post your middleware code.

Hello KenWhitesell,

Thank you for your reply!

I have narrowed down the problem which is more weird now. This the filesystem of the project

In my project/project/urls.py where I have all my top level urls like this

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from ms_identity_web.django.msal_views_and_urls import MsalViews

msal_urls = MsalViews(settings.MS_IDENTITY_WEB).url_patterns()
#
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('authentication.urls')),
    # path('chatbot/', include('chatbot.urls')), # if I comment this out it works
    path(f'{settings.AAD_CONFIG.django.auth_endpoints.prefix}/', include(msal_urls))

] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

The line with the comment is what’s causing the issues. If I uncomment it out while commenting out everything from that apps’ views it still does not work. Literally the chatbot/views.py is empty and still it does not work, it returns empty response.

All the apache error logs do not say anything about this problem.

Do you still need settings.py and apache error logs in light of the aforementioned explanation? Is /chatbot some kind of reserve keyword on apache? Also this is clearly for domain/chatbot/ url handling but it causes error by simply visiting domain.com? I dont understand that at all. Of course it all works fine on a development server.

What about the application logs?

What are the contents of the chatbot/urls.py file? We can start with that alone.

My entire file system is screenshotted above. The main urls.py is in project/project/urls.py and the project/chatbot/urls.py as shown above. Its content is:

from django.urls import path, include
from . import views


urlpatterns = [
    path('send_message/', views.user_message, name='send_message'),
    path('end_chat/', views.end_chat, name='end_chat'),
]

# this is for debugging purposes
# urlpatterns = [
#     path('', views.simple_view, name='simple_view'),
# ]

and the content of its views.py is:

from django.shortcuts import render, redirect
from django.http import JsonResponse, HttpResponse
from .chat_wrapper import ChatWrapper
import jsonpickle


def user_message(request):
    msg = request.GET.get('msg', 'Empty message')
    # request.session.pop("chatbot", None)
    chatbot = request.session.get('chatbot')
    if chatbot is None:
        chatbot = ChatWrapper()# if no framework passed it uses OpenAI
        response = chatbot(msg)["answer"].content
        chatbot_json = jsonpickle.encode(chatbot)
        request.session['chatbot'] = chatbot_json
        data = {'response': response}
    else:
        chatbot = jsonpickle.decode(chatbot)
        response = chatbot(msg)["answer"].content
        chatbot_json = jsonpickle.encode(chatbot)
        request.session['chatbot'] = chatbot_json
        data = {'response': response}
    return JsonResponse(data)


def end_chat(request):
    print("Ending chat", request.session.get('chatbot'))
    if request.session.get('chatbot') is not None:
        del request.session['chatbot']
        request.session.save()
    return redirect('dashboard')

def simple_view(request):
    return HttpResponse("Chatbot is working")

I do not know if it makes any difference but all the functions in the views.py are served via ajax in the system itself.

Finally, I can paste the contents of error-TIPS.log when I make the request to the domain.com:

[Wed Aug 14 18:13:50.303786 2024] [ssl:info] [pid 18036:tid 1340] [client 138.251.14.34:45134] AH01964: Connection to child 62 established (server domain.com:443)
[Wed Aug 14 18:13:50.303786 2024] [ssl:info] [pid 18036:tid 1336] [client 138.251.14.57:34448] AH01964: Connection to child 60 established (server domain.com:443)
[Wed Aug 14 18:13:50.371944 2024] [wsgi:info] [pid 18036:tid 1336] [client 138.251.14.57:34448] mod_wsgi (pid=18036, process='', application='domain.com|'): Loading Python script file 'path/to/wsgi.py'.
[Wed Aug 14 18:13:51.178532 2024] [wsgi:error] [pid 18036:tid 1336] [client 138.251.14.57:34448] Hello you are into middleware <WSGIRequest: GET '/'>\r
[Wed Aug 14 18:13:51.178532 2024] [wsgi:error] [pid 18036:tid 1336] [client 138.251.14.57:34448] hello 1\r
[Wed Aug 14 18:13:51.185867 2024] [wsgi:error] [pid 18036:tid 1336] [client 138.251.14.57:34448] MSAL URLs: [<URLPattern 'sign_in' [name='sign_in']>, <URLPattern 'edit_profile' [name='edit_profile']>, <URLPattern 'redirect' [name='redirect']>, <URLPattern 'sign_out' [name='sign_out']>, <URLPattern 'post_sign_out' [name='post_sign_out']>]\r
[Wed Aug 14 18:13:53.249544 2024] [ssl:info] [pid 4632:tid 404] AH01914: Configuring server domain.com:443 for SSL protocol
[Wed Aug 14 18:13:53.249544 2024] [ssl:info] [pid 4632:tid 404] AH02568: Certificate and private key domain.com:443:0 configured from path/to/certicate and path/to/file.pem
[Wed Aug 14 18:13:54.671534 2024] [ssl:info] [pid 18372:tid 416] AH01914: Configuring server domain.com:443 for SSL protocol
[Wed Aug 14 18:13:53.249544 2024] [ssl:info] [pid 4632:tid 404] AH01914: Configuring server domain.com:443 for SSL protocol
[Wed Aug 14 18:13:53.249544 2024] [ssl:info] [pid 4632:tid 404] AH02568: Certificate and private key domain.com:443:0 configured from path/to/certicate and path/to/file.pem
[Wed Aug 14 18:13:54.671534 2024] [ssl:info] [pid 18372:tid 416] AH01914: Configuring server domain.com:443 for SSL protocol

Anything else you may need please let me know.

I think we need to see the middleware at this point.

Sure, there you go:

from django.conf import settings
from django.urls import reverse_lazy, reverse
from django.http import HttpResponseRedirect
from django.utils.deprecation import MiddlewareMixin
import sys


class AuthRequiredMiddleware(MiddlewareMixin):
    def __init__(self, get_response):
        super().__init__(get_response)
        # Use reverse_lazy to ensure it handles URL reversing dynamically at runtime.
        self.exempt_urls = [
            reverse_lazy('index'),
            reverse_lazy('signup'),
            reverse_lazy('signin'),
            reverse_lazy('signout'),
            reverse_lazy('admin:index'),
            '/auth/sign_in',
            '/auth/redirect'
        ]
        # Automatically exclude static and media URLs
        self.exempt_urls.extend([settings.STATIC_URL, settings.MEDIA_URL])

    def process_request(self, request):
        exempt_urls = [url() if callable(url) else url for url in self.exempt_urls]
        path = request.path_info  # Use path_info to respect server configurations
        print("Hello you are into middleware", request)

        if not (path.startswith(settings.STATIC_URL) or path.startswith(settings.MEDIA_URL)):
            # Check if the path is in the exempt_urls or if the flag is True
            # print(hasattr(request, "identity_context_data"))
            # print(request.session.get('login_framework'))
            flag = (request.session.get('login_framework') == "azure" or request.user.is_authenticated)
            if path not in exempt_urls and not flag:
              print(f"Redirected from: {path}")
              return HttpResponseRedirect(reverse("index"))

        return None

    def __call__(self, request):
        response = self.process_request(request)
        if response:
            return response
        try:
            response = self.get_response(request)
        except Exception as e:
            print(f"Exception in get_response: {e}")
            raise  # Re-raise the exception to ensure it's not swallowed

        return response

Please note that I also comment out the middleware, i.e. disable it and the same behavior persists.

Ok, that’s good to know.

So to be clear at this point, if this line is commented
# path('chatbot/', include('chatbot.urls')),
Your system works as intended, except for those urls, and it works regardless of whether the middleware is enabled.

But, if you uncomment this line, it fails. And it fails regardless of the url being requested, and it fails regardless of whether the middleware is enabled.

Is that a correct summary at this point?

I would still expect to find some detailed information somewhere in a log file.

It might be worth, for testing purposes, to set DEBUG=True to see what additional information may be revealed.

Your summary is correct, yes, truly baffling isnt it?

I have changed in my httpd.conf the log level to:

LogLevel debug
LogLevel wsgi:trace6

and after going through all the logs thoroughly I don’t see any errors only [ssl:info/debug] and [wsgi:error] which I believe is the way wsgi prints out print() statements.

Now debug=True has always been there. I had another irrelevant problem earlier with Azure and MS graph API but because debug is set to True its prints out the whole tracestack and I know exactly what is going wrong. With this problem we are dealing with now, debug=True does not do anything, django does not print out any tracestack simply:

This page isn’t working domain.com didn’t send any data.
ERR_EMPTY_RESPONSE

No errors, nothing.

At this point, I am thinking that the mapping domain.com/chatbot/ might be the issue and I should just recreate this app with another URL mapping because the authentication app which has tons more logic in it, works just fine on both development and production servers.

I find this highly unlikely. There are no url paths with any special significance anywhere within Django.

Does this include the Windows system logs?

At this point, I think we do need to see the actual complete contents of your apache configuration, including all directory and alias directives.

It would also be helpful to see the apache access and error logs for these requests.

Which logs may that be? Win + R → eventvwr brings up a tons of categories for logs such as Windows logs with several subcategories, Applications and Services logs with more subcategories.

Here is the http.conf

# Our settings, modify as required
ServerAdmin admin-email
Define SRVROOT "e:/apache/Apache24"
Define DOCROOT "e:/wwwroot"
ServerRoot "${SRVROOT}"
DocumentRoot "${DOCROOT}"

Listen 80

# these are all default
#LoadModule access_compat_module modules/mod_access_compat.so
# LoadModule actions_module modules/mod_actions.so
LoadModule alias_module modules/mod_alias.so
# LoadModule allowmethods_module modules/mod_allowmethods.so
# LoadModule asis_module modules/mod_asis.so
# LoadModule auth_basic_module modules/mod_auth_basic.so
# LoadModule authn_core_module modules/mod_authn_core.so
# LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_user_module modules/mod_authz_user.so
# LoadModule autoindex_module modules/mod_autoindex.so
# LoadModule cgi_module modules/mod_cgi.so
LoadModule dir_module modules/mod_dir.so
LoadModule env_module modules/mod_env.so
LoadModule include_module modules/mod_include.so
# LoadModule isapi_module modules/mod_isapi.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule mime_module modules/mod_mime.so
#LoadModule negotiation_module modules/mod_negotiation.so
LoadModule setenvif_module modules/mod_setenvif.so

# the following have been added by us for our requirements

# we use rewrite_module in .htaccess
LoadModule rewrite_module modules/mod_rewrite.so

# this is for for httpoxy vulnerability
LoadModule headers_module modules/mod_headers.so

<Directory />
    AllowOverride none
    Require all denied
</Directory>

<Directory "${DOCROOT}">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require local
    Require ip 138.251.0.0/16
</Directory>

# our settings, to control access by IP to particular sites

#<Directory "${DOCROOT}/SiteFolder">
#    Options Indexes FollowSymLinks
#    AllowOverride All
#    Require all granted
#</Directory>

DirectoryIndex index.html

<Files ".ht*">
    Require all denied
</Files>

# our log settings
# new error log on restart and when it reaches 5M in size
# new access log every day
ErrorLog "|bin/rotatelogs.exe logs/error-%Y.%m.%d-%H.%M.%S.log 5M"
LogLevel debug
LogLevel wsgi:trace6
LogFormat "%t %h %v \"%r\" %>s %b" appserver
CustomLog logs/access.log combined

ScriptAlias /cgi-bin/ "${SRVROOT}/cgi-bin/"

<Directory "${SRVROOT}/cgi-bin">
    AllowOverride None
    Options None
    Require all granted
</Directory>

RequestHeader unset Proxy early

TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz

LoadFile "C:/Program Files/Python310/python310.dll"
LoadModule wsgi_module "path/to/venv/lib/site-packages/mod_wsgi/server/mod_wsgi.cp310-win_amd64.pyd"
WSGIPythonHome "path/to/venv"
WSGIPythonPath "path/to/"

# our standard setting, uncomment to use vhosts and defaults
Include conf/extra/httpd-vhosts.conf
Include conf/extra/httpd-default.conf

# For SSL, uncomment the six bottom lines below for Secure (SSL/TLS) connections
# Configure the httpd-ssl.conf file
# Consider Redirects in httpd-vhosts.conf
# Consider BaseURL config in our apps

LoadModule ssl_module modules/mod_ssl.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
Include conf/extra/httpd-ssl.conf
LogFormat "%t %h %v (%{SSL_PROTOCOL}x %{SSL_CIPHER}x) \"%r\" %>s %b" sslappserver
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin

# HTTP header disclosure, uncomment the four bottom lines below
# our setting to prevent disclosure of server type (Apache)
LoadModule security2_module modules/mod_security2.so
LoadModule unique_id_module modules/mod_unique_id.so
SecRuleEngine on
SecServerSignature " "

and http-ssl.conf:

<VirtualHost *:443>
    DocumentRoot "${DOCROOT}/project-folder"
    ServerName   domain.com
    CustomLog "|bin/rotatelogs.exe logs/access-ssl-%Y.%m.%d.log 86400" sslappserver
    SSLEngine on
    SSLCertificateFile      "${SRVROOT}/conf/${SSLCRT}"
    SSLCertificateKeyFile   "${SRVROOT}/conf/${SSLPEM}"
    SSLCertificateChainFile "${SRVROOT}/conf/${SSLINTCRT}"
    #SSLCACertificateFile    "${SRVROOT}/conf/${SSLROOTCRT}"
	
	<Directory "${DOCROOT}/project-folder">
		Require all granted
	</Directory>
	
    <FilesMatch "\.(cgi|shtml|phtml|php|py)$">
        SSLOptions +StdEnvVars
    </FilesMatch>
	
    <Directory "${SRVROOT}/cgi-bin">
        SSLOptions +StdEnvVars
    </Directory>
	
	WSGIScriptAlias / "path/to/wsgi.py"
	# WSGIScriptAlias / "path/to/test_wsgi.py"

	

	# Alias for static files
	Alias /static "path/to/static"
	Alias /media "path/to/media"

	<Directory "path/to/static">
		Require all granted
	</Directory>
	
	<Directory "path/to/media">
		Require all granted
	</Directory>
	
	ErrorLog ${SRVROOT}/logs/error-TIPS.log
	CustomLog ${SRVROOT}/logs/access-TIPS.log combined

    BrowserMatch "MSIE [2-5]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0
    #Un-comment the Header line below when ready to test HSTS
    #max-age 5mins=300, 1day=86400, 1week=604800, 4weeks=2419200, 2years=63072000
    #Header always set Strict-Transport-Security "max-age=300; includeSubDomains;"
</VirtualHost>

<VirtualHost *:443>
    DocumentRoot "${DOCROOT}"
    ServerName   domain-main.com
    CustomLog "|bin/rotatelogs.exe logs/access-ssl-%Y.%m.%d.log 86400" sslappserver
    SSLEngine on
    SSLCertificateFile      "${SRVROOT}/conf/${SSLCRT}"
    SSLCertificateKeyFile   "${SRVROOT}/conf/${SSLPEM}"
    SSLCertificateChainFile "${SRVROOT}/conf/${SSLINTCRT}"
    #SSLCACertificateFile    "${SRVROOT}/conf/${SSLROOTCRT}"
    <FilesMatch "\.(cgi|shtml|phtml|php)$">
        SSLOptions +StdEnvVars
    </FilesMatch>
    <Directory "${SRVROOT}/cgi-bin">
        SSLOptions +StdEnvVars
    </Directory>
    BrowserMatch "MSIE [2-5]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0
    #Un-comment the Header line below when ready to test HSTS
    #max-age 5mins=300, 1day=86400, 1week=604800, 4weeks=2419200, 2years=63072000
    #Header always set Strict-Transport-Security "max-age=300; includeSubDomains;"
</VirtualHost> 

@KenWhitesell I found what breaks the entire system which only creates more questions than answers. I recreated the entire app line by line (yes I know I was desperate) and pinpointed exactly what broke the system…this

from langchain_openai import ChatOpenAI

which is the entire reason for the system as you may have guessed we are developing a chatbot. Do you happen to have any experience with this before I take it up with langchain?

Sorry, I don’t know which one. It has been more than 10 years since I’ve done a Django deployment on Apache, and probably 15 years since I’ve deployed Apache on Windows. (I don’t even have a system in my test lab to check.) But you should probably search around for it - even if you don’t need it now, there’s a chance you may want that information in the future.

That’s a great catch, and excellent diagnostic work!

What I would suggest first is to open the Django shell (python manage.py shell) and enter that import statement to see what happens. I would expect that to produce the expected error message.

Done this and no error import worked. Also in the file that has this import, I moved it to the function which calls it like this

    try:
        from langchain_openai import ChatOpenAI
        print("it got imported no problem")
    except ImportError as e:
        print(f"Import Error: {e}")
    llm = ChatOpenAI(model_name="gpt-4", temperature=0.6)
    #### rest of the function

I do not understand why and this feels such a dirty solution. I have also tried to force uninstall the module and reinstall but that didn’t help either.

Just a real wild guess, but I would assume there’s some dependency or conflict between what it needs and what Django imports causing the sequence that the imports occur to be important. (Possibly a package naming issue.) chasing that down would involve finding what all is being imported when you do this in the shell, and finding out whether that conflicts with another package.

Just out of curiosity though, why does it work fine on the development servers including https ones, if you happen to know or come across a similar case?

No, I’ve never seen or heard of a comparable case. Like I said, it was just a really wild guess that it’s somehow related to the order in which things occur, or possibly the libraries that are or aren’t loaded under specific circumstances. If I were faced with that situation, I’d be inclined to investigate it from a very “low-level” perspective. I might even try running it in a production mode using one of the other mechanisms such as uwsgi or gunicorn to see if it happens there - it’s possible that the root issue is with mod_wsgi itself.
Again - these are all wild conjectures that aren’t based on any specific knowledge or information.

Okay I will try the other web server interfaces. Thank you for all your help and guidance!