FastCGI proxy emulating nginx's try_files
update editorconfig
fix bug
better error printing


browse  log 



You can also use your local clone with git send-email.

#Gemini-Try-Files — FastCGI proxy emulating nginx's try_files

builds.sr.ht status license: AGPL-3.0-only

A simple FastCGI program that essentially emulates nginx's try_files directive for Gemini servers that don't support it.



  • ANSI/CLHS-compliant Common Lisp implementation with Unicode and FFI support. SBCL highly recommended.
  • ASDF 3
  • Quicklisp
  • POSIX-compatible make(1) (optional). Most makes (including GNU Make and BSD Make) support the POSIX standard.

Note: as of 2024-04-03, you will need to clone my updated version of SB-FastCGI or CL-FastCGI (if you use SBCL or another implementation, respectively) into ~/quicklisp/local-projects/ until my upstream contributions get merged.
SB-FastCGI: https://github.com/nytpu/sb-fastcgi
CL-FastCGI: https://github.com/nytpu/cl-fastcgi

First clone the repo:

git clone https://git.sr.ht/~nytpu/gemini-try-files && cd gemini-try-files

#Building a Standalone Executable

There are several ways to build a standalone executable:

  • With SBCL (recommended due to its very fast startup time for standalone executables):


    (or execute util/build.lisp)

  • With another Lisp implementation:

    make LISP="<lisp implementation>" build

    (or execute util/build.lisp)

  • With SBCL or another Lisp implementation but with core compression enabled, if the implementation supports it (not recommended for plain CGI usage due to the much slower startup time): same as with core compression, but use the build-compress target instead of build.

Note that after building a standalone executable, you no longer need a Common Lisp implementation installed on the system, as the executable is by definition standalone. If you built the executable on some local system and then transferred it to a server, then you don't ever need to install Lisp on the server!

#Running From Source

If you have Lisp installed on the server, then having a script that loads and executes util/run.lisp will execute OSB without building a standalone executable, more like most other scripting languages. (make run does this).

#Dumping a Lisp Image or Fast Load file

Loading from source every time is relatively slow, but building a standalone executable bundles an entire copy of the Lisp implementation. As an intermediate step, you can dump just a Lisp image or build a Fast Load file using these commands:

SBCL> (load "util/load.lisp")
SBCL> (asdf:perform 'asdf:image-op :gemini-try-files) ; dump a Lisp image
SBCL> (asdf:perform 'asdf:monolithic-compile-bundle-op :gemini-try-files) ; build a big FASL


The first command line argument to Gemini-Try-Files should be a path to a configuration file. If it isn't provided then it will try to read a file named gtf.sexp in the current directory.

The format of gtf.sexp is as follows: The first s-expression is a list of key-value pairs in association list style. The accepted keys and their values are:

  • :text-type-params — the value is a string containing a series of MIME type parameters that will be appended to the MIME type of any text/* file with a semicolon. Defaults to "lang=en-US;charset=utf-8"
  • :index-name — a string containing the name of the file to treat as the directory index. Defaults to "index.gmi"
  • :error-code — an integer for the Gemini response code to use if a file is not found after trying all the possibilities given. Defaults to 51
  • :error-meta — a string (max length 1024 bytes) that will be given as the response meta if a file is not found after trying all the possibilities given. Defaults to "Not found"
  • :libfcgi — Path to the libfcgi.so shared library. Defaults to /usr/lib/libfcgi.so

The remainder of the config file is a series of filesystem directories to search for incoming requests in.

  • A plain string on its own will simply prepend that directory to the request URI and test if the file exists.
  • A list formatted like (:directory "<path>") will prepend the path to the request URI and append the :index-name to the request (all three separated by directory separators) and test if the index exists. If it exists but the path doesn't end in a /, then it will issue a redirect to the same URI but with an appended slash.
  • A list like ((:with-ext "<ext>") "<path>") will search path like a normal string, but will add the given extension to the filename before testing if it exists.


((:index-name "index.gemini")
 (:text-type-params "charset=cp437"))
(:directory "/var/gemini/example.com/")
((:with-ext "gmi") "/var/gemini/example.com/")
((:with-ext "gemini") "/var/gemini/example.com/")

This example uses index.gemini instead of index.gmi as a directory index, and for all text/* MIME types appends ;charset=cp437.

Given an example request for /files/test it would check the following in sequence, stopping at the first file that exists and returning its contents:

  • /var/gemini/example.com/files/test
  • /var/gemini/example.com/files/test/index.gemini
  • /var/gemini/example.com/files/test.gmi
  • /var/gemini/example.com/files/test.gemini
  • /var/gemini/shared-site-resources/files/test
  • Return 51 Not found to the requester

#Known Issues

Gemini-Try-Files does not sanitize paths before dereferencing them (TODO). Please ensure your Gemini server validates & sanitizes paths before passing them through to the CGI program. If it does not then tree traversal attacks are possible.


The upstream URL of this project is https://git.sr.ht/~nytpu/gemini-try-files. Send suggestions, bugs, patches, and other contributions to ~nytpu/public-inbox@lists.sr.ht or alex@nytpu.com. For help sending a patch through email, see https://git-send-email.io. You can browse the list archives at https://lists.sr.ht/~nytpu/public-inbox.

If you aren't comfortable sending a patch through email, get in touch with a link to your repo and I'll pull the changes down myself!

Copyright (C) 2024 nytpu <alex [at] nytpu.com>.

Licensed under the terms of the GNU Affero General Public License, version 3. You can view a copy of the GNU AGPL in LICENSE or at https://www.gnu.org/licenses/agpl-3.0.html.