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.
| Action | What it does | Attributes (dynamic means an EL value is allowed) | Output |
|---|---|---|---|
|
Rewrites a URL if necessary, prepends the context path to a URL starting with |
|
Written to the page, or kept as a |
|
Adds one query-string parameter to the enclosing |
|
Nothing: it configures its parent |
|
Retrieves a resource by URL (relative in this application, relative in a foreign context, or absolute) and inlines it or exports it |
|
Inline text, a |
|
Sends an HTTP redirect to the browser and ends the processing of the page |
|
A |
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:
@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.
value |
Result | Why |
|---|---|---|
|
|
Context path prepended, then session rewriting |
|
|
Page-relative: left relative, still rewritten |
|
|
Same: the |
Absolute: never rewritten |
||
|
|
The session id goes before the query string |
|
|
Starts with a slash, so it is context-relative and gets the context path (it is not a scheme-relative URL) |
|
|
A literal space in |
empty string |
|
Tomcat’s |
<%@ 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:
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&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&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&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.
<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 +.
| Written in the page | Appended to the URL | Note |
|---|---|---|
|
|
A space becomes |
|
|
Reserved characters and non-ASCII characters are percent-encoded; the hexadecimal digits of this implementation are lower case |
|
|
The name is encoded as well |
|
|
A body value is trimmed |
|
|
A null value is an empty value (7.7) |
|
nothing |
An empty name is ignored, "not an error" (7.7) |
|
|
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&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"onmouseover="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.
| Attribute | Dynamic | Meaning |
|---|---|---|
|
yes, required |
The resource: an absolute URL, a relative URL in this application (context-relative with a leading |
|
yes |
Name of the foreign context, starting with |
|
no |
Name of a scoped variable that receives the content as a |
|
no |
Scope of |
|
no |
Name of a scoped variable that receives a |
|
yes |
Character encoding of the imported content. Applies to |
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:
<%@ 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>
<footer>who=the importing page q=hello extra= ctx=/g5jstlurls</footer>
</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 nestedc:paramvalues are added for the duration of the import only (the firstfooterhasextra=empty, the third hasextra=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 underWEB-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 importfrom thec:param, butwho=null(no request attribute) andjsession=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 than2xxis 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) andcontextPathstill reported the importing application (ctx=/g5jstlurls), as with anyRequestDispatcher.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:
<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.
<%@ 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.
url |
Location header |
|---|---|
|
|
|
|
|
|
|
|
|
|
What c:redirect Cannot Do
-
Choose the status. It always answers
302. Servlet 6.1 addedsendRedirectoverloads that take a status code and aclearBufferflag (303for Post/Redirect/Get,307and308to keep the method), butc:redirecthas no attribute for them: a redirect after a state-changing request, or with a status other than302, 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
sendRedirectthrows. Withjsp:include flush="true"in front of it, wrapped inc: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
urlas optional forc:redirect(andvalueforc:url), but leaving it out is an error:<c:redirect/>failed with aNullPointerException("Cannot invokeString.startsWith(String)becauseurlis null", inUrlSupport.resolveUrl) and<c:url/>with the same one. -
Redirect to a foreign context without a context that exists.
context="/g5jstlurlsb"producedLocation: /g5jstlurlsb/logo.htmlfor a context that exists; for the redirect nothing is dispatched, so the settingcrossContextdoes 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).
Source: Spring Framework API — UrlTag
Source: Apache Struts — url tag
Pitfalls and Error Names
| Symptom | Cause | Fix |
|---|---|---|
Links break when the application is deployed under another context path |
URLs written by hand ( |
|
|
URL rewriting while the session cookie is unconfirmed |
|
Link inside an attribute contains |
|
|
|
A scheme-bearing URL is not rewritten or checked |
Validate the scheme and host in the controller |
Query parameter missing after a fragment (observed): |
|
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 |
|
No |
|
|
Response declared as ISO-8859-1 |
One encoding everywhere (UTF-8) and |
Space or non-ASCII in the |
Only |
Move the variable part into |
|
It starts with a slash, so it is context-relative |
Use a full |
|
A relative |
Both start with |
|
|
|
|
Unreachable host or a non- |
|
|
The imported internal resource set a non- |
Fix the resource; do not use |
|
|
Check the value; the exception is the contract |
|
|
Always give the attribute |
|
Output was flushed before |
Decide the redirect in the controller |
Illegal |
The reader opens at the start tag |
Build the URL with |
Imported |
|
Allowlist; never request data in |
Mojibake in imported text (observed |
The resource declared no charset and sends UTF-8 |
|
Redirect after a |
|
|
What Changed Recently
| Version | Change |
|---|---|
Tags 3.0 (Jakarta EE 10, current in EE 11) |
Package and namespace moved to |
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 |
Servlet 6.1 (Jakarta EE 11) |
|
Servlet 6.1 on Tomcat 11 |
The response character encoding was UTF-8 even for |
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:
| In the books | Today |
|---|---|
|
|
EL only inside JSTL attributes |
EL in template text and in every action’s attributes since JSP 2.0; the container evaluates it, so |
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 |
Cookies only: |
|
|
Containers and JDKs of the JSP 1.2 era, including their proxy settings |
Tomcat 11, Java 21; the proxy properties are the standard |
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.