JSTL URLs, Imports & Redirects

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Standard Tag Library 3.0 (jakarta.tags.core, API 3.0.2, implementation org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1), Jakarta Pages 4.0, Jakarta Expression Language 6.0 and Jakarta Servlet 6.1, running on Apache Tomcat 11.0.26 and Java 21. Everything was verified on 2026-10-08: the facts against chapter 7 (URL Related Actions) of the Tags 3.0 specification and its tag documentation, and every example by building it into a Maven WAR and running it on Tomcat 11.0.26 with curl; the outputs and error messages shown are the ones that run produced (session ids are shortened). Four actions deal with URLs: c:url builds a URL (it prepends the context path, applies session rewriting and appends encoded query parameters), c:param supplies the parameters, c:import reads the content of a URL into the page or into a variable, and c:redirect sends the browser to another URL. They exist because a URL written by hand in a page is almost always wrong in one of three ways: it ignores the context path, it ignores session rewriting, or it does not encode its parameters. The running scenario is the Bookshelf, a small catalogue of books, here as the links, the form action, the page footer and the legacy-link redirect of its views.

The Four Actions at a Glance

All four live in the core library, declared as <%@ taglib prefix="c" uri="jakarta.tags.core" %> (the http://java.sun.com/jsp/jstl/core URI of JSTL 1.2 is still accepted, see Where the Books Are Outdated). They are actions of the JSP page, so they run when the page runs (request time), not when it is translated.

Table 1. The URL actions of the core library (Tags 3.0, chapter 7)
Action What it does Attributes (dynamic means an EL value is allowed) Output

c:url (7.5)

Rewrites a URL if necessary, prepends the context path to a URL starting with /, appends the query parameters given by nested c:param

value (dynamic), context (dynamic), var, scope

Written to the page, or kept as a String in var

c:param (7.7)

Adds one query-string parameter to the enclosing c:url, c:import or c:redirect, URL-encoding both name and value

name (dynamic, required), value (dynamic); the value may also be the body

Nothing: it configures its parent

c:import (7.4)

Retrieves a resource by URL (relative in this application, relative in a foreign context, or absolute) and inlines it or exports it

url (dynamic, required), context (dynamic), var, scope, varReader, charEncoding (dynamic)

Inline text, a String in var, or a Reader in varReader

c:redirect (7.6)

Sends an HTTP redirect to the browser and ends the processing of the page

url (dynamic), context (dynamic)

A 302 response with a Location header

The Bookshelf views of this page live under WEB-INF/views and are reached through a controller, as everywhere in this section (see the servlets page for the pattern). The scratch application is two WARs on one Tomcat: g5jstlurls (the catalogue, with Book as a record and a controller that starts a session and forwards to a view) and g5jstlurlsb (a second application that stands for a separate cover-image service, used for the foreign-context examples). The only servlet the views link to is an echo that prints what it received:

The controller and the echo servlet of the scratch application
@WebServlet("/demo/*")
public class DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String page = req.getPathInfo().substring(1);
        req.getSession();                       // so that URL rewriting has a session to rewrite for
        req.setAttribute("books", List.of(
                new Book(1, "Dune", "Frank Herbert", 1965),
                new Book(2, "Neuromancer", "William Gibson", 1984),
                new Book(3, "Snow Crash & Co", "Neal Stephenson", 1992)));
        req.setAttribute("who", "the importing page");
        req.getRequestDispatcher("/WEB-INF/views/" + page + ".jsp").forward(req, resp);
    }
}

@WebServlet("/books/*")
public class BooksServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().println("path=" + req.getPathInfo() + " q=" + req.getParameter("q")
                + " page=" + req.getParameter("page") + " who=" + req.getAttribute("who")
                + " jsession=" + (req.getRequestedSessionId() != null));
    }
}

c:url: Building a URL

c:url takes a value and returns a URL a browser can use from the page that contains it. The specification (7.5) gives it three jobs. First, a URL starting with / is context-relative, so the context path is prepended (with a context path of /foo, /ads/logo.html becomes /foo/ads/logo.html). Second, a relative URL is rewritten by calling HttpServletResponse.encodeURL, which appends the session id as a path parameter when the container decides the browser may not be returning its session cookie. Third, nested c:param actions append query parameters, encoded. An absolute URL (one that starts with a scheme and a colon) is not rewritten, "to prevent situations where an external URL could be rewritten and expose the session ID" (7.5), so only relative URLs get a session id and only they get the context path.

flowchart TD start["value, and the optional context attribute"] --> abs{"Does the value start with a scheme such as https:?"} abs -- yes --> keep["Kept as it is: no context path, no session id"] abs -- no --> slash{"Does it start with a slash?"} slash -- yes --> ctx["The context path is prepended, or the context attribute's value"] slash -- no --> rel["Page-relative: stays relative"] ctx --> enc["response.encodeURL adds ;jsessionid=... only while the session is not confirmed by a cookie"] rel --> enc keep --> par["Nested c:param actions are appended to the query string, name and value encoded"] enc --> par par --> dest{"Is var present?"} dest -- yes --> store["The URL is kept as a String in var, in the given scope"] dest -- no --> print["The URL is written to the page; for c:redirect it is sent as the Location header instead"]
Table 2. What c:url does with each kind of value; observed on Tomcat 11.0.26 for a request to /g5jstlurls/demo/url-basics by a client that sent no session cookie
value Result Why

/books

/g5jstlurls/books;jsessionid=9246DDE3...

Context path prepended, then session rewriting

books

books;jsessionid=9246DDE3...

Page-relative: left relative, still rewritten

../books

../books;jsessionid=9246DDE3...

Same: the .. is the browser’s business

https://example.org/books

https://example.org/books

Absolute: never rewritten

/books?page=2

/g5jstlurls/books;jsessionid=9246DDE3...?page=2

The session id goes before the query string

//example.org/x

/g5jstlurls//example.org/x;jsessionid=9246DDE3...

Starts with a slash, so it is context-relative and gets the context path (it is not a scheme-relative URL)

/books/Snow Crash

/g5jstlurls/books/Snow Crash

A literal space in value is not encoded: "it is the user’s responsibility" (7.5)

empty string

http://localhost:18080/g5jstlurls/demo/;jsessionid=9246DDE3...

Tomcat’s encodeURL turns the empty relative URL into an absolute URL of the current directory (observed)

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<p>context-relative: <c:url value="/books"/></p>
<p>page-relative: <c:url value="books"/></p>
<p>absolute: <c:url value="https://example.org/books"/></p>
<p>already has query: <c:url value="/books?page=2"/></p>
<p>no scheme, host-like: <c:url value="//example.org/x"/></p>
<c:url var="listUrl" value="/books"/>
<p>var: ${listUrl}</p>
<c:url var="reqUrl" value="/books" scope="request"/>
<p>scope request: ${requestScope.reqUrl}</p>
curl -s http://localhost:18080/g5jstlurls/demo/url-basics
<p>context-relative: /g5jstlurls/books;jsessionid=9246DDE3...</p>
<p>page-relative: books;jsessionid=9246DDE3...</p>
<p>absolute: https://example.org/books</p>
<p>already has query: /g5jstlurls/books;jsessionid=9246DDE3...?page=2</p>
<p>no scheme, host-like: /g5jstlurls//example.org/x;jsessionid=9246DDE3...</p>
<p>var: /g5jstlurls/books;jsessionid=9246DDE3...</p>
<p>scope request: /g5jstlurls/books;jsessionid=9246DDE3...</p>

With var the URL is stored instead of printed, as a String in the scope named by scope (page, request, session or application; page by default). A stored URL can be reused for several links, put in a request attribute for an included fragment, or passed to c:redirect. The body of c:url is JSP content that the container evaluates, trims and then processes; in practice the body holds only c:param actions.

URL Rewriting and the Session

Whether the ;jsessionid=... part appears depends on the container’s session tracking, not on the tag. On the first response to a client that has not yet returned the cookie, Tomcat cannot know that cookies work, so encodeURL adds the id. Once the browser sends the cookie back, the same page produces clean URLs. The page links.jsp makes the difference visible; the first request carries no cookie, the second one does:

The Bookshelf list with links built by c:url and c:param (WEB-INF/views/links.jsp)
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<ul>
<c:forEach var="b" items="${books}">
  <c:url var="detailUrl" value="/books/${b.id}"/>
  <li><a href="${detailUrl}"><c:out value="${b.title}"/></a>
  <c:url var="searchUrl" value="/books"><c:param name="q" value="${b.author}"/><c:param name="page" value="1"/></c:url>
  (<a href="<c:out value='${searchUrl}'/>">more by <c:out value="${b.author}"/></a>)</li>
</c:forEach>
</ul>
<form action="<c:url value='/books'/>" method="get"><input name="q"></form>
<img src="<c:url value='/static/cover.png'/>" alt="">
curl -s -c cj http://localhost:18080/g5jstlurls/demo/links > /dev/null     # first visit: the cookie is stored
curl -s -b cj http://localhost:18080/g5jstlurls/demo/links                  # the cookie is sent back
curl -s http://localhost:18080/g5jstlurls/demo/links                        # a client without cookies
<!-- with the cookie -->
<li><a href="/g5jstlurls/books/1">Dune</a>
(<a href="/g5jstlurls/books?q=Frank+Herbert&amp;page=1">more by Frank Herbert</a>)</li>
<form action="/g5jstlurls/books" method="get"><input name="q"></form>
<img src="/g5jstlurls/static/cover.png" alt="">

<!-- without the cookie -->
<li><a href="/g5jstlurls/books/1;jsessionid=FBD23F5B...">Dune</a>
(<a href="/g5jstlurls/books;jsessionid=FBD23F5B...?q=Frank+Herbert&amp;page=1">more by Frank Herbert</a>)</li>

Two consequences follow. A page that links every internal URL through c:url works with and without cookies, and one that writes /g5jstlurls/books by hand works only with cookies (and breaks if the application is deployed under another context path). And the session id in the path is a security problem of its own: the Servlet specification says URL rewriting "exposes session identifiers in logs, bookmarks, referer headers, cached HTML, and the URL bar" and "should not be used" where cookies are supported (7.1.3). The fix is to turn it off, not to stop using c:url: with <tracking-mode>COOKIE</tracking-mode> in the session-config of web.xml, the same page produced /g5jstlurlsd/books/1 and /g5jstlurlsd/books?q=Frank+Herbert&amp;page=1 for a cookie-less client (observed on a further deployment of the same WAR with only that element added). c:url still adds the context path, which is its main value. The sessions section of Servlet Fundamentals discusses tracking modes, and the Struts and Spring tags that do the same job are in The Same Job in Spring, Struts and Thymeleaf.

The one element that turned rewriting off (web.xml of the further deployment)
<session-config>
  <tracking-mode>COOKIE</tracking-mode>
</session-config>

The context Attribute: Another Web Application

context names another web application on the same container, so that a link to it gets its context path rather than the current one. Both the context name and the URL must start with a slash; anything else is a JspTagException. A rewritten URL for another context carried no session id in the observed run, because Tomcat’s encodeURL only rewrites URLs of the same application.

<p><c:url value="/logo.html" context="/g5jstlurlsb"/></p>
<c:catch var="e"><c:url value="logo.html" context="/g5jstlurlsb"/></c:catch>
<p>${e['class'].name}: ${e.message}</p>
<c:catch var="e2"><c:url value="/logo.html" context="g5jstlurlsb"/></c:catch>
<p>${e2['class'].name}: ${e2.message}</p>
<p>/g5jstlurlsb/logo.html</p>
<p>jakarta.servlet.jsp.JspTagException: In URL tags, when the "context" attribute is specified, values of both "context" and "url" must start with "/".</p>
<p>jakarta.servlet.jsp.JspTagException: In URL tags, when the "context" attribute is specified, values of both "context" and "url" must start with "/".</p>

c:param: Parameters and Encoding

c:param adds one parameter to its parent. The parent is c:url, c:import or c:redirect; there is no standalone use. name is required; the value comes from value or, when that attribute is absent, from the body (which the container evaluates and trims). Both name and value are URL-encoded by the tag (7.7), which is what makes it safer than concatenating strings: the specification’s own example is a country name containing a space that must become a +.

Table 3. How c:param encodes; observed on Tomcat 11.0.26 with the page encoding set to UTF-8
Written in the page Appended to the URL Note

<c:param name="q" value="${books[2].title}"/> where the title is Snow Crash & Co

q=Snow+Crash+%26+Co

A space becomes +, an ampersand becomes %26

<c:param name="q" value="a&b=c#d/é+ü?"/>

q=a%26b%3dc%23d%2f%c3%a9%2b%c3%bc%3f

Reserved characters and non-ASCII characters are percent-encoded; the hexadecimal digits of this implementation are lower case

<c:param name="a b&c" value="1"/>

a+b%26c=1

The name is encoded as well

<c:param name="q"> a b </c:param>

q=a+b

A body value is trimmed

<c:param name="q" value="${null}"/>

q=

A null value is an empty value (7.7)

<c:param name="" value="x"/>

nothing

An empty name is ignored, "not an error" (7.7)

<c:param name="z"/>

z=

No value and no body: an empty value

<c:url var="detail" value="/books/${books[0].id}">
  <c:param name="q" value="${books[2].title}"/>
  <c:param name="page" value="2"/>
</c:url>
<p><a href="<c:out value='${detail}'/>">Dune</a></p>
<p>raw: ${detail}</p>
<p>special: <c:url value="/books"><c:param name="q" value="a&b=c#d/é+ü?"/></c:url></p>
<p><a href="/g5jstlurls/books/1;jsessionid=9246DDE3...?q=Snow+Crash+%26+Co&amp;page=2">Dune</a></p>
<p>raw: /g5jstlurls/books/1;jsessionid=9246DDE3...?q=Snow+Crash+%26+Co&page=2</p>
<p>special: /g5jstlurls/books;jsessionid=9246DDE3...?q=a%26b%3dc%23d%2f%c3%a9%2b%c3%bc%3f</p>

Two details of the output matter. The & between parameters is a raw & in the URL, which is correct as a URL but wrong as HTML, so a URL used in an attribute must go through c:out (the first line above shows &, the second the raw value); see Escaping a URL in HTML. And the URL contains the session id before the query string, so a hand-made comparison of URLs in tests has to allow for it.

Which Character Encoding c:param Uses

Non-ASCII characters are encoded with the character encoding of the response, not with a fixed UTF-8. Three variants of the same line, <c:param name="q" value="é"/>, in pages that differ only in their directives:

<%-- enc3.jsp: page source read as UTF-8, plain text/html --%>
<%@ page contentType="text/html" pageEncoding="UTF-8" %>
<%-- enc2.jsp: page source read as UTF-8, response declared as ISO-8859-1 --%>
<%@ page contentType="text/html; charset=ISO-8859-1" pageEncoding="UTF-8" %>
<%-- enc.jsp: no pageEncoding: the UTF-8 bytes of the file are read as two ISO-8859-1 characters --%>
<%@ page contentType="text/html" %>
<%-- each followed by --%>
<p>${pageContext.response.characterEncoding}: <c:url value="/books"><c:param name="q" value="é"/></c:url></p>
<p>UTF-8: /g5jstlurls/books;jsessionid=1D2EDBF0...?q=%c3%a9</p>
<p>ISO-8859-1: /g5jstlurls/books;jsessionid=CC162C16...?q=%e9</p>
<p>UTF-8: /g5jstlurls/books;jsessionid=23F9D067...?q=%c3%83%c2%a9</p>

The first result is the right one: the encoded bytes are those of the response encoding, and the servlet that receives the request must decode them with the same charset (the request-encoding section of the character encoding page covers setCharacterEncoding and the request-character-encoding element). The second is consistent but wrong for a UTF-8 application, since a servlet configured for UTF-8 reads %e9 as an invalid sequence. The third is the classic double encoding (%c3%83%c2%a9), caused by a missing pageEncoding, not by c:param. On Tomcat 11.0.26 the response encoding was UTF-8 even for a plain text/html page without a charset; do not rely on that, set pageEncoding and contentType (or the page-encoding property) explicitly. The details are in Character Encoding.

Where c:param Cannot Help

c:param only appends to the end of the URL string, and it knows nothing about a fragment or an existing query string. Observed:

<p>fragment: <c:url value="/books#top"><c:param name="q" value="x"/></c:url></p>
<p>existing query: <c:url value="/books?page=2"><c:param name="q" value="x"/></c:url></p>
<p>fragment: /g5jstlurls/books;jsessionid=9246DDE3...#top?q=x</p>
<p>existing query: /g5jstlurls/books;jsessionid=9246DDE3...?q=x&page=2</p>

The first result is broken: the parameter ended up inside the fragment, so the browser never sends it. Add the fragment after the URL has been built (${url}#top, inside c:out when it goes in an attribute). The second works, but the new parameter came out in front of the existing one, so do not rely on the order, and prefer to give every parameter to c:param rather than mixing a hand-written query string with it. The c:param description in the tag documentation says it adds a parameter to "a containing 'import' tag’s URL"; it is valid in c:url and c:redirect as well, as the specification says.

Escaping a URL in HTML

c:param encodes for the URL, not for HTML. c:url does not escape its value at all. The two together decide whether a link is safe, and the behaviour is easy to check with an attacker-controlled value. The page xss.jsp builds a URL from a request parameter and prints it twice:

<c:url var="u" value="${param.u}"><c:param name="q" value='"><script>x</script>'/></c:url>
<a href="${u}">raw</a>
<a href="<c:out value='${u}'/>">escaped</a>
curl -s -G --data-urlencode 'u=/books"onmouseover="x' http://localhost:18080/g5jstlurls/demo/xss
curl -s 'http://localhost:18080/g5jstlurls/demo/xss?u=javascript:alert(1)'
<a href="/g5jstlurls/books"onmouseover="x?q=%22%3e%3cscript%3ex%3c%2fscript%3e">raw</a>
<a href="/g5jstlurls/books&#034;onmouseover=&#034;x?q=%22%3e%3cscript%3ex%3c%2fscript%3e">escaped</a>

<a href="javascript:alert(1)?q=%22%3e%3cscript%3ex%3c%2fscript%3e">raw</a>
<a href="javascript:alert(1)?q=%22%3e%3cscript%3ex%3c%2fscript%3e">escaped</a>

The c:param value was encoded, so the <script> in it is harmless; the value of c:url was not, so a quote in it broke out of the attribute (first request, raw line). c:out fixes the breakout (the escaped line), but not the second problem: a javascript: URL has a scheme, so c:url leaves it alone and c:out has nothing to escape. The rules are: write c:url with a literal value (or an internal path chosen by the controller), pass everything variable through c:param, output the result with c:out in an attribute, and never put request data in value. A URL that must come from the user is validated in the controller against a list of allowed hosts or schemes. The same advice for Thymeleaf is in Link URLs and Template Security and XSS.

c:import: Reading a Resource into the Page

c:import (7.4) retrieves a resource and either writes it into the page where the tag is, or exports it as a String (var) or a Reader (varReader). It is the JSTL answer to what jsp:include cannot do. The comparison of the two, with the dispatcher-type demonstration and a first look at the errors, is in Includes, Forwards and Page Composition. This page does not repeat that comparison; it adds the rest of the attribute set, the encoding rules, the foreign-context case and the security analysis.

Table 4. The c:import attributes (Tags 7.4; request-time column from the tag documentation)
Attribute Dynamic Meaning

url

yes, required

The resource: an absolute URL, a relative URL in this application (context-relative with a leading /, or relative to the current page), or, with context, a context-relative URL in another application. An empty or null url raises NullAttributeException

context

yes

Name of the foreign context, starting with /; the url must then start with / as well

var

no

Name of a scoped variable that receives the content as a String

scope

no

Scope of var: page (default), request, session or application

varReader

no

Name of a scoped variable that receives a Reader. It is visible only inside the body of c:import, which the container closes at the end tag

charEncoding

yes

Character encoding of the imported content. Applies to String and Reader alike

flowchart LR imp["c:import url"] --> kind{"Kind of URL"} kind -- "scheme://host/path" --> abs["java.net.URLConnection: a new GET request, no session, no request attributes, nothing from the page"] kind -- "/path or path" --> same["RequestDispatcher.include in this application: attributes, parameters and session shared, WEB-INF reachable"] kind -- "/path with context=/other" --> foreign["The other context's dispatcher: needs crossContext=true on Tomcat"] abs --> res{"var, varReader or neither?"} same --> res foreign --> res res -- neither --> inl["Written inline to the page"] res -- var --> str["A String in the scoped variable"] res -- varReader --> rdr["A Reader, visible only in the body of the action"]

The Kinds of URL, Side by Side

The page import.jsp imports one small view, footer.jsp, a one-line fragment that prints the request attribute who, the parameters q and extra, and the context path, and then the echo servlet through an absolute URL, and a page of the second application:

One fragment imported in the different ways (WEB-INF/views/import.jsp, abridged)
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<h1>same context, inline</h1>
<c:import url="/WEB-INF/views/footer.jsp"/>
<h1>same context, page-relative</h1>
<c:import url="footer.jsp"/>
<h1>same context, with param</h1>
<c:import url="/WEB-INF/views/footer.jsp"><c:param name="extra" value="a b&c"/></c:import>
<h1>var</h1>
<c:import url="/WEB-INF/views/footer.jsp" var="footer" scope="request"/>
<pre>[${fn:length(footer)}]</pre>
<pre><c:out value="${footer}"/></pre>
<h1>absolute (no request environment)</h1>
<c:import url="http://localhost:18080/g5jstlurls/books/abs">
  <c:param name="q" value="from import"/>
</c:import>
<h1>foreign context</h1>
<c:import url="/logo.html" context="/g5jstlurlsb"/>
<c:import url="/echo.jsp" context="/g5jstlurlsb"><c:param name="q" value="x y"/></c:import>
<h1>varReader</h1>
<c:import url="/WEB-INF/views/footer.jsp" varReader="r">
  reader class: ${r.getClass().name}
</c:import>
curl -s 'http://localhost:18080/g5jstlurls/demo/import?q=hello'
<h1>same context, inline</h1>
<footer>who=the importing page q=hello extra= ctx=/g5jstlurls</footer>
<h1>same context, page-relative</h1>
<footer>who=the importing page q=hello extra= ctx=/g5jstlurls</footer>
<h1>same context, with param</h1>
<footer>who=the importing page q=hello extra=a b&c ctx=/g5jstlurls</footer>
<h1>var</h1>
<pre>[72]</pre>
<pre>
&lt;footer&gt;who=the importing page q=hello extra= ctx=/g5jstlurls&lt;/footer&gt;
</pre>
<h1>absolute (no request environment)</h1>
path=/abs q=from import page=null who=null jsession=false
<h1>foreign context</h1>
<b>logo from the other context</b>
ctx=/g5jstlurls q=x y who=the importing page session=true
<h1>varReader</h1>
  reader class: java.io.StringReader

Read the output as the contract of the three kinds:

  • A relative URL in the same application is an include through the RequestDispatcher (7.4, "Relative URL - same context"). The page-relative form is resolved against the importing page, here /WEB-INF/views. The importing page’s attributes (who) and request parameters (q) are visible to the fragment, and the nested c:param values are added for the duration of the import only (the first footer has extra= empty, the third has extra=a b&c, decoded again by the container). It can import a JSP, a servlet or a static file from anywhere in the application, including under WEB-INF.

  • An absolute URL is a new HTTP request made by the server to itself or to someone else (7.4, "Absolute URL"). The echo servlet saw q=from import from the c:param, but who=null (no request attribute) and jsession=false (no session cookie): "none of the current execution environment" reaches it, even when it is the same host and context. It follows redirects, and a response status other than 2xx is an error.

  • A relative URL in a foreign context is dispatched to the other application, and the observed fragment shows that on Tomcat 11.0.26 the importing request’s attributes still arrived (who=the importing page) and contextPath still reported the importing application (ctx=/g5jstlurls), as with any RequestDispatcher.include. The specification says only the request environment of the importing page is available to the target; the attribute behaviour above is what the container did.

var and varReader differ in what they cost and promise. The String is kept in the scope you ask for, can be reused and is not escaped when written with ${footer} in template text, so print it with c:out unless it is trusted markup (the escaped line above). The Reader avoids the buffering of the whole content (7.2.2: the benefit "is not guaranteed", it depends on the implementation) and exists only between the start and end tags; on Tomcat 11.0.26 it was a java.io.StringReader, so the content had been read anyway. A c:param inside a varReader import is rejected when the page is compiled:

<c:import url="/WEB-INF/views/footer.jsp" varReader="r"><c:param name="x" value="1"/>${r}</c:import>
Validation error messages from TagLibraryValidator for [c] in [/WEB-INF/views/import-reader-param.jsp]
4: Illegal "c:param" tag within "c:import varReader=..." tag

The reason is in the specification: the reader must be available at the start tag, so the URL cannot still be changing. Build the URL with c:url and its parameters first, and pass it to c:import (7.4).

charEncoding

An imported resource is text, so c:import must know its encoding. With charEncoding absent it uses the charset of the Content-Type response header of the resource, and if there is none (or it is unsupported) ISO-8859-1 (7.4, "Character Encoding"). The specification expects charEncoding to be needed "normally only" for non-HTTP absolute URLs, but any HTTP resource that sends UTF-8 without declaring it needs it as well. A servlet that writes café in UTF-8 under a plain Content-Type: text/plain:

<p>default: <c:import url="http://localhost:18080/g5jstlurls/books/utf8-nocharset"/></p>
<p>charEncoding: <c:import url="http://localhost:18080/g5jstlurls/books/utf8-nocharset" charEncoding="UTF-8"/></p>
<p>default: café</p>
<p>charEncoding: café</p>

The cure is better done at the source: declare the charset in the Content-Type of everything the application serves (see the character encoding page).

Errors

A failing import throws; the specification lists the cases ("Null and Error Handling", 7.4). The page import-404.jsp imports six failing resources inside c:catch:

<c:catch var="e"><c:import url="/WEB-INF/views/nothere.jsp"/></c:catch>
<p>1: ${e['class'].name}: ${e.message}</p>
<c:catch var="e2"><c:import url="/WEB-INF/views/status404.jsp"/></c:catch>
<p>2: ${e2['class'].name}: ${e2.message}</p>
<c:catch var="e3"><c:import url="http://localhost:18080/g5jstlurls/books/nonexistent/../../nothing"/></c:catch>
<p>3: ${e3['class'].name}: ${e3.message}</p>
<c:catch var="e4"><c:import url="http://localhost:1/x"/></c:catch>
<p>4: ${e4['class'].name}: ${e4.message}</p>
<c:catch var="e5"><c:import url=""/></c:catch>
<p>5: ${e5['class'].name}: ${e5.message}</p>
<c:catch var="e6"><c:import url="/WEB-INF/views/footer.jsp" context="/nosuchctx"/></c:catch>
<p>6: ${e6['class'].name}: ${e6.message}</p>
<p>1: jakarta.servlet.jsp.JspException: jakarta.servlet.ServletException: JSP file [/WEB-INF/views/nothere.jsp] not found</p>
<p>2: jakarta.servlet.jsp.JspTagException: 404 /WEB-INF/views/status404.jsp</p>
<p>3: jakarta.servlet.jsp.JspException: Problem accessing the absolute URL "http://localhost:18080/g5jstlurls/books/nonexistent/../../nothing". java.io.FileNotFoundException: http://localhost:18080/g5jstlurls/books/nonexistent/../../nothing</p>
<p>4: jakarta.servlet.jsp.JspException: Problem accessing the absolute URL "http://localhost:1/x". java.net.ConnectException: Connection refused</p>
<p>5: org.apache.taglibs.standard.tag.common.core.NullAttributeException: The "url" attribute illegally evaluated to "null" or "" in <import></p>
<p>6: jakarta.servlet.jsp.JspException: jakarta.servlet.ServletException: JSP file [/WEB-INF/views/footer.jsp] not found</p>

Case 2 shows the status rule for an internal resource: a non-2xx status set by the imported page becomes a JspTagException with the status and path. Case 3 and 4 are the absolute-URL failures, wrapping FileNotFoundException (a 404) or an IOException. Case 6, a context that does not exist, was reported as a missing file ("JSP file …​ not found") rather than as a missing context; a context that exists but is not reachable gives a different message, see below. Because the message of case 3 contains the URL, never print an exception message to the visitor when the URL held anything secret.

Foreign Contexts

context="/g5jstlurlsb" imports a page of another web application, through that application’s RequestDispatcher. Tomcat allows it only if the importing application’s Context has crossContext="true", which the scratch WAR sets in META-INF/context.xml; the includes page says that case was not exercised there, so here are both outcomes. The same import-foreign.jsp was deployed twice, once with the setting and once without:

The Context element of the importing application (META-INF/context.xml)
<Context crossContext="true"/>
<c:catch var="e"><c:import url="/logo.html" context="/g5jstlurlsb"/></c:catch>
<c:if test="${e != null}"><p>${e['class'].name}: ${e.message}</p></c:if>
<!-- crossContext="true" -->
<b>logo from the other context</b>

<!-- crossContext not set (the default is false) -->
<p>jakarta.servlet.jsp.JspTagException: Unable to get RequestDispatcher for Context: "/g5jstlurlsb" and URL: "/logo.html". Verify values and/or enable cross context access.</p>

The specification itself warns that foreign-context imports "may not work in all containers" and suggests an absolute URL as the portable alternative, at the price of a real HTTP round trip and none of the page’s environment (7.4). Cross-context dispatching weakens the isolation between applications, so enable it only for applications that trust each other. If the container sits behind a proxy and an absolute URL must leave through it, the JVM needs the networking properties (http.proxyHost, http.proxyPort and their https twins, 7.2.4), which apply to every URLConnection of the container, not only to this page.

Security: Never Build the URL From Request Data

c:import is the one action on this page that makes the server do something on behalf of a request. With a url taken from a parameter it is both server-side request forgery (CWE-918) and a file-disclosure hole, as the page import-file.jsp shows:

<%-- user-controlled url: the SSRF and file-read pitfall --%>
<c:import url="${param.src}"/>
curl -s 'http://localhost:18080/g5jstlurls/demo/import-file?src=/WEB-INF/web.xml'
curl -s 'http://localhost:18080/g5jstlurls/demo/import-file?src=file:///etc/hosts'
curl -s 'http://localhost:18080/g5jstlurls/demo/import-file?src=http://localhost:18080/manager/html'
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" ...        <- HTTP 200: the deployment descriptor
(the content of /etc/hosts of the server)                       <- HTTP 200: a file: URL is read by URLConnection
Server returned HTTP response code: 401 for URL: http://localhost:18080/manager/html   <- HTTP 500, but the request was made

The first request returned the application’s own descriptor (a relative import is dispatched inside the container, so the rule that hides WEB-INF from browsers does not apply), the second read a local file through the file: scheme of URLConnection, and the third made the container call an internal service (the 401 is the target’s answer; with an unprotected internal host or a cloud metadata address the content would have been returned). The defence is the one given for jsp:include: never pass request data as url; map a short key to a fixed target in the controller or in an allowlist and import only that; if the URL must vary, build it with c:url from a fixed base and parameters. The example of an allowlist is in the security section of the includes page. Also remember that c:import of an external site puts its content on your origin: unescaped markup from another site becomes cross-site scripting (print it with c:out unless it is trusted), and its slowness becomes yours (a request thread waits on the remote server, with the JDK default of no timeouts). A view is the wrong place to call a remote service; call it from the controller or a service with a client that has timeouts, and give the view the result.

c:redirect: Sending the Browser Elsewhere

c:redirect (7.6) sends an HTTP redirect to the client and ends the page: the tag calls sendRedirect and returns SKIP_PAGE, so nothing after it is evaluated or written. It follows the same rewriting rules as c:url: a URL starting with / gets the context path (the specification requires this when the implementation uses sendRedirect), a relative URL is passed on as it is, an absolute URL is untouched, and c:param children are appended, encoded. It has no var and no scope: its result is the response.

The legacy-link redirect of the Bookshelf (WEB-INF/views/redirect.jsp)
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
before
<c:if test="${param.isbn == 'legacy'}">
  <c:redirect url="/books/1"><c:param name="q" value="old link"/></c:redirect>
</c:if>
after
curl -s -i 'http://localhost:18080/g5jstlurls/demo/redirect?isbn=legacy'
curl -s -i -b cj 'http://localhost:18080/g5jstlurls/demo/redirect?isbn=legacy' | grep -i -E '^(HTTP|Location)'
curl -s 'http://localhost:18080/g5jstlurls/demo/redirect?isbn=ok'
curl -s -L -b cj 'http://localhost:18080/g5jstlurls/demo/redirect?isbn=legacy'
HTTP/1.1 302
Location: /g5jstlurls/books/1;jsessionid=17168219...?q=old+link
Content-Type: text/html;charset=UTF-8
Content-Length: 0

HTTP/1.1 302
Location: /g5jstlurls/books/1?q=old+link

before

after

path=/1 q=old link page=null who=null jsession=true

The 302 carries an empty body and the text "before" and "after" never reaches the client: the buffer is cleared, as with sendRedirect. The second request, with the cookie, got a clean Location; encodeRedirectURL-style rewriting follows the same session rule as c:url. The third request shows that the page behaves normally when the condition is false. The redirect target is another request: its parameters are in the query string, its attributes are gone (who=null in the last line), which is why a redirect needs the session, a flash value or the URL to pass data.

Table 5. What the response of c:redirect looks like for each kind of URL, observed on Tomcat 11.0.26
url Location header

/books/1 with c:param q=old link

/g5jstlurls/books/1;jsessionid=17168219...?q=old+link (no session id when the cookie was sent)

https://example.org/x?a=1

https://example.org/x?a=1

/books/2

/g5jstlurls/books/2;jsessionid=100A70BD...

//evil.example/x

/g5jstlurls//evil.example/x;jsessionid=7D093D75... (treated as a path of this application)

/logo.html with context="/g5jstlurlsb"

/g5jstlurlsb/logo.html

What c:redirect Cannot Do

  • Choose the status. It always answers 302. Servlet 6.1 added sendRedirect overloads that take a status code and a clearBuffer flag (303 for Post/Redirect/Get, 307 and 308 to keep the method), but c:redirect has no attribute for them: a redirect after a state-changing request, or with a status other than 302, belongs in the servlet. The overloads are in Servlet Fundamentals.

  • Redirect after output has been committed. If the response is already committed (a flushed buffer, an included resource that flushed), the redirect is impossible and sendRedirect throws. With jsp:include flush="true" in front of it, wrapped in c:catch:

    <jsp:include page="/WEB-INF/views/footer.jsp" flush="true"/>
    <c:catch var="e"><c:redirect url="/books/1"/></c:catch>
    <p>${e['class'].name}: ${e.message}</p>
    <footer>who=the importing page q= extra= ctx=/g5jstlurls</footer>
    <p>java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed</p>

    The page had already answered 200, so the browser got the content and no redirect. This is also why a redirect in a view, after any output, is a design smell: decide before the view starts, in the controller.

  • Redirect without a target. The TLD and the tag documentation declare url as optional for c:redirect (and value for c:url), but leaving it out is an error: <c:redirect/> failed with a NullPointerException ("Cannot invoke String.startsWith(String) because url is null", in UrlSupport.resolveUrl) and <c:url/> with the same one.

  • Redirect to a foreign context without a context that exists. context="/g5jstlurlsb" produced Location: /g5jstlurlsb/logo.html for a context that exists; for the redirect nothing is dispatched, so the setting crossContext does not apply (observed with and without it).

Open Redirects

c:redirect with a url from a request parameter is an open redirect (CWE-601): the page is a trusted site that sends visitors to an address of the attacker’s choosing, which phishing links use.

<c:redirect url="${param.next}"/>
curl -s -i 'http://localhost:18080/g5jstlurls/demo/redirect-abs?next=https://example.org/x?a=1' | grep -i -E '^(HTTP|Location)'
HTTP/1.1 302
Location: https://example.org/x?a=1

Any absolute URL is sent as it is. The context-path rule gives some protection to scheme-relative forms (the //evil.example/x row above was turned into a path of this application), but that is a side effect of the implementation, not a safeguard to rely on. Redirect to a path chosen by the application (a fixed target, or a key looked up in a map), and when a return address must come from the user, accept only a path that starts with a single /, contains no scheme, host or backslash, and resolves to a known page of the application.

The Same Job in Spring, Struts and Thymeleaf

Every Java view technology has its own copy of these tags, because the context path and session rewriting are universal problems. Spring’s JSP tag library has spring:url (class UrlTag) with the same value, context, var and scope attributes plus javaScriptEscape and spring:param; Struts 2 has the s:url tag, with its includeParams and other attributes (see the Struts documentation, Tag Developers Guide); Thymeleaf writes the same links with the @{...} link expression, which adds the context path and rewrites the URL through encodeURL (the Thymeleaf pages cover it in Link URLs). In a Spring MVC application that still renders JSP, the redirect is done by the controller (redirect: prefix or RedirectView) rather than by c:redirect; Web UI Frameworks compares the options. The Struts pages of this section cover s:url (Tags Overview & Generic Tags) and the redirect and redirectAction result types (Results & Result Types).

Pitfalls and Error Names

Table 6. URL-action pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Tomcat 11.0.26)
Symptom Cause Fix

Links break when the application is deployed under another context path

URLs written by hand (href="/books")

c:url value="/books", or a base tag set from c:url

;jsessionid=... in links, logs and the Referer header (observed)

URL rewriting while the session cookie is unconfirmed

<tracking-mode>COOKIE</tracking-mode> in session-config; keep c:url for the context path

Link inside an attribute contains & instead of &, or a quote breaks the attribute (observed)

c:url output printed raw; c:url does not escape value

<a href="<c:out value='${u}'/>">; never put request data in value

javascript: link survives c:out (observed)

A scheme-bearing URL is not rewritten or checked

Validate the scheme and host in the controller

Query parameter missing after a fragment (observed): #top?q=x

c:param appends to the end of the string

Add the fragment after building the URL

Parameter order changes when the URL already has a query (observed)

The new parameter is inserted before the old ones

Give all parameters to c:param

%c3%83%c2%a9 for é (observed)

No pageEncoding: the file was read as ISO-8859-1 and encoded again

pageEncoding="UTF-8" and a UTF-8 response

%e9 that the servlet cannot decode (observed)

Response declared as ISO-8859-1

One encoding everywhere (UTF-8) and setCharacterEncoding before reading parameters

Space or non-ASCII in the value of c:url is not encoded (observed)

Only c:param encodes

Move the variable part into c:param

//host/x becomes /context//host/x (observed)

It starts with a slash, so it is context-relative

Use a full https://host/x URL for another host

JspTagException: In URL tags, when the "context" attribute is specified, values of both "context" and "url" must start with "/" (observed)

A relative url or a context without a leading slash

Both start with /

JspTagException: Unable to get RequestDispatcher for Context ... enable cross context access (observed)

crossContext is false on the importing application

crossContext="true" for trusting applications, or an absolute URL

JspException: Problem accessing the absolute URL ... (observed)

Unreachable host or a non-2xx status

c:catch; a client with timeouts in the controller

JspTagException: 404 /path (observed)

The imported internal resource set a non-2xx status

Fix the resource; do not use sendError in fragments

NullAttributeException: The "url" attribute illegally evaluated to "null" or "" (observed)

c:import url="" or a null expression

Check the value; the exception is the contract

NullPointerException from UrlSupport.resolveUrl (observed)

c:url without value or c:redirect without url

Always give the attribute

IllegalStateException: Cannot call sendRedirect() after the response has been committed (observed)

Output was flushed before c:redirect

Decide the redirect in the controller

Illegal c:param within c:import varReader=... (observed)

The reader opens at the start tag

Build the URL with c:url first

Imported /WEB-INF/web.xml, file: URL or internal host (observed)

c:import url="${param.src}"

Allowlist; never request data in url

Mojibake in imported text (observed café)

The resource declared no charset and sends UTF-8

charEncoding="UTF-8", or declare the charset at the source

Redirect after a POST is a 302

c:redirect cannot set a status

sendRedirect(location, 303, true) in the servlet

What Changed Recently

Table 7. Changes that touch the URL actions
Version Change

Tags 3.0 (Jakarta EE 10, current in EE 11)

Package and namespace moved to jakarta.*; tag-library URIs renamed to jakarta.tags.core and the other four (the Tags 3.0 changelog entry "Rename Tag URIs"); Java 11 baseline; JPMS module descriptor. The four URL actions kept their attributes

Tags 3.0.2 API, implementation 3.0.1

Still the latest final releases on Maven Central on 2026-10-08; the 3.1.0-M2 API milestone exists

Tags 3.1 (planned, Jakarta EE 12)

Announced changes concern java.time in the date formatting tags (Tags 3.1 specification page); none changes the URL actions

Servlet 6.1 (Jakarta EE 11)

sendRedirect overloads with a status and a clearBuffer flag; c:redirect does not use them and still answers 302

Servlet 6.1 on Tomcat 11

The response character encoding was UTF-8 even for text/html without a charset (observed), which affects what c:param encodes; declare the encoding explicitly

Where the Books Are Outdated

The JSTL book of the bibliography teaches c:import, c:param, c:redirect and c:url for JSTL 1.0 on JSP 1.2. The concepts it contributes (the relative, context-relative, foreign-context and absolute kinds of import, var, varReader, charEncoding, URL rewriting, the context path and encoding) all survive and are covered above. What changed around them:

Table 8. JSTL 1.0 URL actions in the books and today
In the books Today

http://java.sun.com/jstl/core and the _rt twin library http://java.sun.com/jstl/core_rt, with the EL evaluated by the tag library itself

jakarta.tags.core, one library. The old URIs still resolve in the JSTL 3.0.1 jar (c-1_0.tld, c-1_0-rt.tld, and the 1.2 URI http://java.sun.com/jsp/jstl/core), but a ${...} in an attribute of the 1.0 library is rejected on a Pages 4.0 page (observed: "attribute [value] does not accept any expressions"), while the _rt twin accepts <%= ... %> (observed, with scripting enabled)

EL only inside JSTL attributes

EL in template text and in every action’s attributes since JSP 2.0; the container evaluates it, so <a href="${u}"> works. Escape it with c:out

Importing RSS feeds, other sites and their content directly in a view

Treat as a security and reliability problem (SSRF, no timeouts, content on your origin): call the remote service from a controller or service and give the view data

Session id in the URL as the normal fallback and encodeURL on every link

Cookies only: <tracking-mode>COOKIE</tracking-mode>; the Servlet specification now says URL rewriting should not be used where cookies are supported. Keep c:url for the context path

javax.servlet.* classes in the error messages and Config constants

jakarta.servlet.*; the messages shown on this page are the current ones

Containers and JDKs of the JSP 1.2 era, including their proxy settings

Tomcat 11, Java 21; the proxy properties are the standard http.proxyHost family of the JDK

Inconsistencies in the Official Documentation

Four details of the official documents did not match the behaviour of the current implementation. The TLD and the tag documentation mark value of c:url and url of c:redirect as optional, but omitting them is a NullPointerException (observed). The tag documentation describes c:param as adding a parameter to "a containing 'import' tag’s URL", although the specification and the implementation also allow it in c:url and c:redirect. The specification says a foreign-context import sees only the request environment of the importing page, but the request attributes arrived in the observed run. And the specification still points to the "Java 2 SDK" networking documentation and RFC 2396 for encoding, while the implementation percent-encodes with lower-case hexadecimal digits and + for a space.

References