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
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
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:
- 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.
- 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.
- Same as (2) but for directories.
- 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.
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
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
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:
- 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.
- 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.
- Same as (2) but for directories.
- 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.