Chapter 6. Immutant Web
- 6.1. Introduction
- 6.2. Context Path
- 6.3. Virtual Host
- 6.4. Registering Handlers
- 6.5. Deregistering Handlers
- 6.6. Context/Sub-Context Path Example
- 6.7. Sessions
- 6.8. Locating dirs within the application root
- 6.9. Serving static resources
- 6.10. Mounting Servlets instead of Ring handlers
- 6.11. Overriding the max number of HTTP threads
6.1. Introduction
Immutant allows applications to respond to web requests, typically via Ring handlers, but standard Java Servlets are supported, too. Each application can dynamically register any number of handlers, each with a unique context path. This allows you to have multiple Ring endpoints that share the same deployment lifecycle.
Immutant provides a session implementation that provides automatic data sharing across nodes in a cluster.
In this chapter, the term application refers to the full deployment itself, and endpoint refers to a particular web application based around a single Ring handler as its entry point. For most applications, the two will be the same thing.
6.2. Context Path
The context path is a prefix on the path portion of a url that is
used as a mechanism to route requests to the proper endpoint when
more than one endpoint is being served by the same 'container'. If
you are running only one endpoint in a container (which is the
typical strategy when deploying a Clojure endpoint via Jetty), the
context path is the root context - /
.
A properly constructed endpoint needs no knowledge of the context path it is mounted under - the container is responsible for routing requests to the endpoint and providing the endpoint specific fragment of the url's path to it. This allows the endpoint to be moved between contexts (or mounted at multiple contexts at once) without any modification to the endpoint code itself.
6.2.1. The Top-Level Context Path
Every application deployed has a context path assigned on its behalf. Since handlers can be registered dynamically from anywhere during the lifecycle of the application, we reserve a context path for every application, whether that application registers web handlers or not.
If no context path is provided, a default context path based on the name of
the deployment is used. For example: an application deployed using a
descriptor named some-app.clj
will be given the context path
/some-app
. An application deployed using an archive named
some-other-app.ima
will be given the context path /some-other-app
.
See Deployment for the details of deploying Clojure applications.
You can override the default context path via the :context-path
key in
either in the deployment descriptor or the :immutant
section of the
application's project.clj
(see Initialization for the details on setting
configuration values).
This context path is considered the top-level context path - you have the
option to bind a handler to a sub-context path that will be nested within
the top-level path. The full context is stripped from the url's path before
the request is processed, and the context and remaining path info are made
available as part of the request map via the :context
and :path-info
keys, respectively.
6.2.2. The Sub-Context Path
When you register a handler with Immutant, you can optionally provide a sub-context path for which the handler will be responsible. This sub-context path is appended to the top-level context path for purposes of routing requests to the handler, and allows you to have multiple endpoints within a single application.
See below for an example demonstrating how the context path, sub-context path, and path info all work together.
6.3. Virtual Host
In addition to segmenting applications by context path, you can also segment
by virtual host. To specify the virtual host (or hosts) for an application,
set the :virtual-host
key in either in the deployment descriptor or the
:immutant
section of the application's project.clj
(see Initialization
for the details on setting configuration values). :virtual-host
must be
a string specifying a single host or a vector of strings specifying multiple
hosts. Two applications with different virtual hosts can use the same
context path without collision.
6.4. Registering Handlers
To register a Ring handler, you simply call immutant.web/start, which takes one or two arguments, plus some options:
sub-context-path
- the sub-context path within the application's context path where the handler should be attached. Optional - if omitted, "/" is assumed. Only one handler can be attached to any given sub-context path - providing an already attached sub-context will replace the previously registered handler.handler
- the Ring handler to invoke when requests come in on the sub context path. If a symbol is passed, it'll be resolved to a var to facilitate auto-reloading.
The options are a subset of those provided by the popular ring-server:
Option | Default | Description |
---|---|---|
:init | nil | A function called immediately after the handler is mounted |
:destroy | nil | A function called immediately after the handler is stopped |
:stacktraces? | true in :dev | Display stacktraces in browser when exception is thrown |
:auto-reload? | true in :dev | Automatically reload source files |
:reload-paths | all dirs on CP | A seq of src paths to monitor for changes |
These options can also be specified in the :ring
map in
project.clj
, useful when porting existing Ring apps.
Auto-reloading is enabled by default in development mode, which is
determined by the presence of the :dev
profile among the active
ones in your Leiningen project. The lein-immutant plugin activates
:dev
by default when you deploy your app, but you can override
this using the with-profile
higher-order task. You can also set
the environment variable, LEIN_NO_DEV
, to disable these defaults,
regardless of whether :dev
is active.
Let's take a look at start
in use. For the following example, assume
the application has a top-level context path of /my-app
:
(ns my.ns (:require [immutant.web :as web])) ;; handle requests at the root sub-context (/). ;; this handler will receive any request that the app ;; receives at /my-app/* *except* for anything captured by another ;; sub-context. (web/start my-root-handler) ;; handle requests at the /somewhere sub-context. ;; this handler will receive any request that the app ;; receives at /my-app/somewhere/*. (web/start "/somewhere" my-other-handler) ;; If you want to see your changes immediately while in a REPL, ;; the symbol referring to your handler should resolve to a var. ;; You only need to explicitly set :auto-reload? when not in ;; development mode. (web/start #'your-handler :auto-reload? true)
You can deregister a registered handler at any time. Immutant will deregister any remaining handlers for you when your application is undeployed.
6.5. Deregistering Handlers
You can deregister a Ring handler via the immutant.web/stop function, which takes zero or one arguments:
sub-context-path
- the sub-context path within the application's context path where the handler was attached. Optional - if omitted, "/" is assumed.
An example of using stop
:
(ns my.ns (:require [immutant.web :as web])) ;; deregisters the handler attached to the root sub-context (/) (web/stop) ;; deregisters the handler attached to the /somewhere sub-context (web/stop "/somewhere")
6.6. Context/Sub-Context Path Example
Now that we've introduced registering ring handlers, we can give an example that makes it clear how the context and sub-context paths work, and how the path info gets set.
First, we'll set the context path for the entire application in our
project.clj
:
(defproject someapp "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.4.0"]] :immutant {:init someapp.core/start :context-path "/foo"})
Now we'll register two ring handlers with different sub-contexts, both
of which will be available under the parent context we set in project.clj
:
(ns someapp.core (:require [immutant.web :as web])) (defn make-handler [sub-context] (fn [{:keys [context path-info] :as request}] {:status 200 :content-type "text/plain" :body (pr-str {:mounted-sub-context sub-context :request-context context :request-path-info path-info})})) (defn start [] ;; responds to /foo/ (web/start "/" (make-handler "/")) ;; responds to /foo/bar/ (web/start "/bar" (make-handler "/bar")))
When we deploy the app to Immutant, we can see how it sets the :context
and
:path-info
keys in the request map:
$ curl http://localhost:8080/foo/baz {:mounted-sub-context "/", :request-context "/foo", :request-path-info "/baz"} $ curl http://localhost:8080/foo/baz/sheep {:mounted-sub-context "/", :request-context "/foo", :request-path-info "/baz/sheep"} $ curl http://localhost:8080/foo/bar/baz {:mounted-sub-context "/bar", :request-context "/foo/bar", :request-path-info "/baz"} $ curl http://localhost:8080/foo/bar/baz/sheep {:mounted-sub-context "/bar", :request-context "/foo/bar", :request-path-info "/baz/sheep"} $ curl http://localhost:8080/foo/bar {:mounted-sub-context "/bar", :request-context "/foo/bar", :request-path-info "/"} $ curl http://localhost:8080/foo/ {:mounted-sub-context "/", :request-context "/foo", :request-path-info "/"}
6.7. Sessions
Immutant provides a session store that can be used with the Ring session middleware (and any other middleware that uses
ring.middleware.session
, like Sandbar). The Immutant session store
uses the session provided by the underlying JBoss AS7 servlet
container, which automatically replicates session data across a
cluster. You create the store by calling
immutant.web.session/servlet-store, and use it by passing it as the
:store
option to ring.middleware.session/wrap-session
:
(ns my.ns (:require [ring.middleware.session :as ring-session] [immutant.web :as web] [immutant.web.session :as immutant-session])) (web/start (ring-session/wrap-session #'my-handler {:store (immutant-session/servlet-store)}))
Note: since this store is managed by the servlet container, the
session cookie (JSESSIONID
by default) is itself managed at the
servlet level. Any options other than :store
passed to
ring.middleware.session/wrap-session
(:cookie-attrs
,
:cookie-name
, or :root
) won't affect the actual cookie used to
store the session id client-side. To set the cookie name or
attributes, see session options.
6.7.1. Setting session timeout and cookie attributes
By default, sessions using the servlet-store time out after 30 minutes. To alter that for an application, call immutant.web.session/set-session-timeout! and pass it a new minute value:
(ns my.ns (:require [immutant.web.session :as immutant-session])) (immutant-session/set-session-timeout! 1440) ;; 1 day
You can also override the attributes used for the session cookie via immutant.web.session/set-session-cookie-attributes!, giving it the following keyword arguments:
Attribute | Default | Description |
---|---|---|
:cookie-name | "JSESSIONID" | The name used for the cookie. |
:domain | none | The domain name where the cookie is valid. |
:http-only | false | Should the cookie be used only for http? |
:max-age | -1 (expire on browser close) | The amount of time the cookie should be retained by the client, in seconds. |
:path | the context path | The path where the cookie is valid. |
:secure | false | Should the cookie be used only for secure connections? |
This function can be called multiple times, and will only alter the attributes passed to it:
(ns my.ns (:require [immutant.web.session :as immutant-session])) (immutant-session/set-session-cookie-attributes! :cookie-name "my-session") (immutant-session/set-session-cookie-attributes! :http-only true :max-age 86400)
Changes made by either of these functions apply to all of the web endpoints deployed within the application, since they all share the same session.
6.7.2. Duplicate session cookies
Since sessions using the servlet-store are managed at the container level, the cookie used to convey the session id to the client is managed outside of Ring. However, Ring is unaware of this management, and will attempt to send its own cookie (named "ring-session" by default). This can cause to cookies with the same value (the session id) but different names ("JSESSIONID" and "ring-session") to be sent to the client. This situation is harmless, other than the extra few bytes needed for each request.
You can prevent this by ensuring Ring's :cookie-name
is the same
name used by the session container. Immutant will detect this case,
and prevent the cookie duplication.
There are three options for achieving this cookie name parity:
(ns my.ns (:require [ring.middleware.session :as ring-session] [immutant.web :as web] [immutant.web.session :as immutant-session])) ;; option 1: pass the default "JSESSIONID" name to Ring's wrap-session (web/start (ring-session/wrap-session #'my-handler {:store (immutant-session/servlet-store) :cookie-name "JSESSIONID"})) ;; option 2: set the container's cookie name to Ring's default of "ring-session" (immutant-session/set-session-cookie-attributes! :cookie-name "ring-session") ;; option 3: use a non-default cookie name (web/start (ring-session/wrap-session #'my-handler {:store (immutant-session/servlet-store) :cookie-name "session-schmession"})) (immutant-session/set-session-cookie-attributes! :cookie-name "session-schmession")
6.8. Locating dirs within the application root
When a web server is embedded within an application, it's fine to make assumptions about relative paths because the current working directory for both the app and the web server is the same.
But this is not the case for Immutant, or any app server, because multiple applications may be deployed on it simultaneously. The app server is a single process, with a single current working directory, and an application should not assume that the server's current directory matches its root.
But certain libraries require actual filesystem paths to directories at runtime, so you need to be able to determine the path to the application root. You can do so via the immutant.util/app-relative function.
6.8.1. An example
This won't work on Immutant because "src" is a relative path, and it will resolve relative to the server's working directory, not the application's:
(noir.server/load-views "src/my_project_name/views")
But this will work on Immutant, because it returns an absolute path:
(require '[immutant.util :as util]) (noir.server/load-views (util/app-relative "src/my_project_name/views"))
6.9. Serving static resources
Web applications often need to serve static resources such as images, stylesheets, or javascript files. Ring applications use middleware to accomplish this.
In Immutant, the recommended approach is to store your resources
beneath resources/public/
, and then reference that path using
ring.middleware.resource/wrap-resource:
(ring.middleware.resource/wrap-resource app "public")
This works because the resources/
directory is automatically added
to the application's effective classpath at deployment.
NOTE: Prior to version 1.2, Ring middleware didn't support
applications mounted at a context path other than the root. The
1.0.x versions of Immutant ship with Ring 1.1.8, and provide a
drop-in replacement, immutant.web/wrap-resource
, that is no longer
available in later Immutant versions.
6.10. Mounting Servlets instead of Ring handlers
Though mounting a Ring handler at a particular [sub] context path will suffice for the majority of Clojure web apps, some may find it too limiting and require the full capabilities of the Java Servlet API. Of course, like any servlet container, you can always wrap your servlets up into a war file and deploy it to Immutant. But you don't have to. You can mount any servlet to a context path via the immutant.web/start-servlet function.
6.10.1. Pedestal
Pedestal is a brand-new Clojure web framework for building rich
client applications requiring asynchronous processing, server-sent
events, response streaming, etc. As such, its entry-point is not a
simple Ring handler but a special servlet comprised of a stack of
"interceptors". So to mount a Pedestal servlet when your Leiningen
project is deployed to Immutant, you might put the following in
your immutant.init
namespace (or just invoke it at a REPL):
(ns immutant.init (:require [immutant.web :as web] [io.pedestal.service.http :as http] [hello.service :as service])) (web/start-servlet "/" (::http/servlet (http/create-servlet service/service)))
The io.pedestal.service.http/create-servlet
function returns a
map of many things, including the actual Servlet instance
associated with the keyword, :io.pedestal.service.http/servlet
.
6.11. Overriding the max number of HTTP threads
The servlet container used by JBossAS uses a bounded thread pool for
handling HTTP connections. The default maximum bound for that pool
is 512 * the number of cores. If you need to reduce or expand that
limit, you can do so via a system property:
org.immutant.web.http.maxThreads
.
There are several ways to set that system property - you can pass it
to lein immutant run
:
lein immutant run -Dorg.immutant.web.http.maxThreads=9000
or to standalone.sh
:
$IMMUTANT_HOME/jboss/bin/standalone.sh -Dorg.immutant.web.http.maxThreads=9000
or add it to standalone.xml
:
<extensions> ... </extensions> <system-properties> <property name='org.immutant.web.http.maxThreads' value='123'/> </system-properties>
Note that if you have changed the name of the HTTP connector from "http" for some reason, you will need to replace "http" in the system property with that new name.