JBoss.orgCommunity Documentation

Chapter 6. Immutant Web

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:

OptionDefaultDescription
:initnilA function called immediately after the handler is mounted
:destroynilA function called immediately after the handler is stopped
:stacktraces?true in :devDisplay stacktraces in browser when exception is thrown
:auto-reload?true in :devAutomatically reload source files
:reload-pathsall dirs on CPA 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:

AttributeDefaultDescription
:cookie-name"JSESSIONID"The name used for the cookie.
:domainnoneThe domain name where the cookie is valid.
:http-onlyfalseShould 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.
:paththe context pathThe path where the cookie is valid.
:securefalseShould 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.

Immutant 1.1.4