bottle + CGI always matches / route

Question:

I can’t get bottle to match any other route than “/” when deploying in a CGI environment. (I’m stuck with the hosting provider, FastCGI or WSGI are not on offer, unfortunately).

Bottle lives in a subdirectory lib – I have dropped the bottle.py from bottle-0.12.18.tar.gz there.

Python is either 3.5.3 (provider) or 3.8.2 (localhost)

I have the following in a .htaccess

Options +ExecCGI
AddHandler cgi-script .py

Options -Indexes

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteRule ^static/ - [L]  
  RewriteRule .* application.py [L]
</IfModule>

and the following application.py

#!/usr/bin/python3

#setup lib path
import os
import os.path
import sys
if 'SCRIPT_NAME' in os.environ:
    MY_DIR = os.path.dirname(os.path.realpath(os.environ['SCRIPT_FILENAME']))
else:
    MY_DIR = os.environ['PWD']    
sys.path.append(os.path.join(MY_DIR,'lib'))


from bottle import Bottle

app = Bottle()

@app.route('/test')
def test():
    return '<b>matched @app.route("/test") - Testing: One, Two</b>!'

@app.route('/')
def index():
    r = ""
    for p in os.environ.keys():
        r += "{0} : {1}n".format(p,os.environ[p])
    return '<b>matched @app.route("/")</b>n<pre>'+r+'</pre>'

app.run(server='cgi')

This always return the output from index(), no matter what URL I request.

From the output

matched @app.route("/")

APP_ENGINE : phpcgi
APP_ENGINE_VERSION : 7.3
AUTH_TYPE : Basic
CFG_CLUSTER : cluster003
DOCUMENT_ROOT : /home/somethinguxiz/www-dev
ENVIRONMENT : production
GATEWAY_INTERFACE : CGI/1.1
HOME : /homez.907/somethinguxiz
HTTP_ACCEPT_ENCODING : gzip, deflate, br
HTTP_ACCEPT_LANGUAGE : en,de-DE;q=0.7,de;q=0.3
HTTP_ACCEPT : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
HTTP_DNT : 1
HTTP_FORWARDED : for=51.xxx.xxx.xxx; proto=https; host=dev.something.else
HTTP_HOST : dev.something.else
HTTP_REMOTE_IP : 90.xxx.xxx.xxx
HTTPS : on
HTTP_UPGRADE_INSECURE_REQUESTS : 1
HTTP_USER_AGENT : Mozilla/5.0 (X11; Linux x86_64; rv:74.0) Gecko/20100101 Firefox/74.0
HTTP_X_FORWARDED_FOR : 90.xxx.xxx.xxx
HTTP_X_FORWARDED_PORT : 443
HTTP_X_FORWARDED_PROTO : https
HTTP_X_PREDICTOR : 1
HTTP_X_REMOTE_IP : 51.xxx.xxx.xxx
HTTP_X_REMOTE_PORT : 64440
HTTP_X_REMOTE_PROTO : https
PATH : /usr/local/bin:/usr/bin:/bin
PHP_VER : 5_TEST
PWD : /homez.907/somethinguxiz/www-dev
QUERY_STRING : 
REDIRECT_STATUS : 200
REDIRECT_URL : /test
REGISTER_GLOBALS : 0
REMOTE_ADDR : 90.xxx.xxx.xxx
REMOTE_PORT : 17926
REMOTE_USER : csomething
REQUEST_METHOD : GET
REQUEST_URI : /test
SCRIPT_FILENAME : /home/somethinguxiz/www-dev/application.py
SCRIPT_NAME : /application.py
SCRIPT_URI : https://dev.something.else:443/test
SCRIPT_URL : /test
SERVER_ADDR : 51.xxx.xxx.xxx
SERVER_ADMIN : [email protected]
SERVER_NAME : dev.something.else
SERVER_PORT : 443
SERVER_PROTOCOL : HTTP/1.1
SERVER_SIGNATURE : 
SERVER_SOFTWARE : Apache
UID : somethinguxiz

I.e. the script does see the REQUEST_URI yet it does not fire test() but index()!?

Any ideas, pointers?

EDIT:
unless it is removed somewhere, PATH_INFO is missing in the above list although it should be there if I understand the CGI specs correctly. It’s also what bottle uses in its match() method. It falls back to / if not present (which explains that always index() is called.

EDIT: move solution text to answer

Asked By: Snakebite

||

Answers:

PATH_INFO is only set if the path goes beyond the cgi script. While my original intent was to have that invisible I have made peace with having an application name here. Plus, there is a difference in having [L] in the rewrite rule compared to [END].

Thus I settled with the following .htaccess:


Options +ExecCGI                                                                                                                     
AddHandler cgi-script .py                                                                                                            
AcceptPathInfo on                                                                                                                    

Options -Indexes                                                                                                                     

<IfModule mod_rewrite.c>                                                                                                             
    RewriteEngine On                                                                                                                 
    RewriteOptions IgnoreInherit                                                                                                     
    RewriteBase /                                                                                                              
    RewriteRule ^demo/(static/.*)$ $1 [END]                                                                                        
    RewriteRule ^demo(/.*)$ application.py$1 [END]                                                                                   
    RewriteRule ^demo$ application.py [END]                                                                                           
    RewriteRule !^demo$ demo [R,END]                                                                                                  
</IfModule>                                                                                                                          

It allows to serve static content from httpd while application logic is in application.py – hidden as demo. Anything else redirects to the front door.

Application is at .../demo,
Pages within the application are at .../demo/path/to/page, and
Ressources served by httpd are at .../demo/static/path/to/ressource

Answered By: Snakebite

I had this problem too, and found a solution that let me completely eliminate any path leftovers for the script itself. The following Apache Rewrite directives in the .htaccess file in the CGI script’s directory did the trick:

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteBase /

  RewriteRule ^$ script.py                # (1)
  RewriteCond %{REQUEST_FILENAME} !-f     # (2)
  RewriteCond %{REQUEST_FILENAME} !-d     # (3)
  RewriteRule ^(.*)$ script.py/$1 [QSA,L] # (4)
</IfModule>

Explanations of the numbered lines:

  1. The first RewriteRule sends any index requests (such as for https://www.example.com with or without the trailing slash) directly to the script. This takes care of one half of my needs: to have the "front door" be handled by my script as well. If you have a static index page, leave this Rule out.
  2. This Condition takes care of static files: if it already exists at the request location, the next rewrite is skipped. Since the next Rule is how the script ever sees non-index requests, this makes Apache serve it directly instead of sending it to the script. If you have no static files to serve, or want the application’s own logic to serve static files, leave this Condition out.
  3. Same as (2) but for directories.
  4. The other half of the solution. This rewrites any requests that aren’t already served by the previous rules: anything like /<non-empty-request> will run as if it was /script.py/<non-empty-request>.

Thus, 1st line’s Rule sending https://www.example.com to my script lets its @app.route('/') handler do its job properly regardless of static index files, and the 4th line’s Rule makes sure all other requests for non-static files are properly fed to the script, and are handled by all other registered route handlers.

As a side note, that RewriteBase / isn’t strictly necessary, but I like to have that default made explicit. See this answer to How does RewriteBase work in .htaccess for a thorough breakdown of RewriteBase and what it’s good for.

Answered By: SevenSidedDie
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.