Includes, Forwards & Page Composition
|
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 Pages 4.0, Jakarta Standard Tag Library 3.0 (Jakarta Tags), Jakarta Expression
Language 6.0 and Jakarta Servlet 6.1, running on Apache Tomcat 11.0.26 and Java 21 with the JSTL implementation
org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1. Everything was verified on 2026-10-08: the facts against the
specifications and the Tomcat 11 documentation, and every example by building it into a Maven WAR and requesting it with
curl on that Tomcat; the outputs shown are the outputs of that run. A page is almost never written in one piece: a header,
a footer, a navigation bar and a book card are shared by many pages. Jakarta Pages offers two include mechanisms with very
different semantics (a translation-time directive and a request-time action), a forward action, implicit includes that
the container applies to a whole group of pages, and the JSTL c:import action that can also reach resources outside the
web application. This page explains each one, compares them, and ends with the layout patterns the Bookshelf uses. It
builds on the dispatcher material of Servlet Fundamentals and on the
project layout of Getting Started.
Three Ways to Put Content in a Page
The specification summarises the choice in one table (Table JSP.1-10): the include directive is a translation-time include
of a static object, whose content is parsed by the container; the include action is a request-time include of a static or
dynamic object, whose output is placed in the current output. A third way, c:import, comes from the tag library and works
like the action for resources of the same web application but can also fetch absolute URLs.
| include directive | jsp:include action |
c:import action |
|
|---|---|---|---|
Syntax |
|
|
|
When it happens |
Translation time: the page is recompiled if the file changes (Tomcat does this, the specification only allows it) |
Every request |
Every request |
What is included |
The text of the file, parsed as JSP |
The output of the target resource, which runs separately |
The output of a relative target (same mechanism as |
Relative path is relative to |
The including file |
The including page |
The including page (relative URL), or the context (starting with |
Accepts an expression |
No: a literal path |
Yes ( |
Yes ( |
Scope and variables |
The same translation unit: page-scope variables, scriptlet variables, tag prefixes and declarations are shared |
Separate: only request, session and application attributes and request parameters are visible; page scope is not |
Relative: as |
Can change status or headers |
Yes, it is part of the page |
No, such calls are ignored (specification 5.4) |
Relative: a status other than 2xx raises an exception (observed). Absolute: only the body is imported |
Can reach outside the web application |
No |
No (same web application) |
Yes: absolute URLs, and other contexts through |
Result can be kept in a variable |
No |
No |
Yes: |
Typical use |
Constants, tag-library declarations, small shared fragments |
Header, footer, widgets that need their own logic |
Content from another system, or text to process before output |
The rest of the page takes the mechanisms one by one. The examples use the Bookshelf of this section: a controller servlet
that puts a list of books in the request and forwards to a view under WEB-INF, as on the Getting Started page. The
servlets in the scratch application map /v/* to a fixed set of views and /site/* to the layout, and every view
lives under /WEB-INF/views.
The include Directive (Static Include)
The directive <%@ include file="relativeURLspec" %> inserts the text of the named file into the page at translation time
(Pages 4.0, 1.10.3). The file is parsed exactly as if its text had been typed at the place of the directive, so everything
that is in force in the including page at that point is in force inside the file, and everything the file declares is in
force in the rest of the page. The container, not the browser, resolves the path, and the file is subject to the access
control of the container, which is why shared segments belong under WEB-INF. The path rules of section 1.2.1 apply: a path
starting with / is relative to the web application, any other path is relative to the file that contains the directive.
The Bookshelf keeps its shared pieces in /WEB-INF/jspf. The convention .jspf ("JSP fragment") marks a segment that
is not a page by itself:
<%-- header.jspf: no page directive; the segment becomes part of the including page --%>
<c:set var="site" value="Bookshelf" scope="request"/>
<% String siteName = "Bookshelf"; %>
<header>Welcome to the <c:out value="${site}"/></header>
<%-- footer.jspf --%>
<footer>(c) <%= siteName %> -- translation unit: <%= this.getClass().getSimpleName() %></footer>
<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
<%@ include file="/WEB-INF/jspf/header.jspf" %>
<main>
<c:forEach items="${books}" var="book"><p>${book.title}</p></c:forEach>
<p>siteName from the segment's scriptlet variable: <%= siteName %></p>
</main>
<%@ include file="/WEB-INF/jspf/footer.jspf" %>
curl -s http://localhost:18080/g3includes/v/static-include
<header>Welcome to the Bookshelf</header>
<main>
<p>Dune</p><p>Neuromancer</p><p>siteName from the segment's scriptlet variable: Bookshelf</p>
</main>
<footer>(c) Bookshelf -- translation unit: static_002dinclude_jsp</footer>
The output shows what "the same translation unit" means. The segment used the c prefix that only the including page
declares, the footer used a scriptlet variable declared in the header, and this.getClass() is the one servlet class that
Tomcat generated from static-include.jsp (the dash in the file name became _002d). The scriptlet variable is shown to
prove the point; new code does not use scriptlets (see the scripting elements page of this part), and the c:set and the
tag prefix are the legitimate form of the same sharing.
Rules and Failure Modes of the Static Include
| Topic | Behaviour |
|---|---|
Syntax mixing |
A page in standard syntax can include a file written in XML syntax and the other way round; the container detects the syntax of each file (1.10.5). XML-syntax pages are covered on the JSP documents page of this part |
Start and end tags |
A start tag and its end tag must appear in the same file (1.3.3): a segment cannot open a |
Same declaration twice |
Including the same segment twice repeats its declarations. The scriptlet variable of |
Tag prefix redefinition |
A |
The |
It is not an expression. |
Change detection |
The specification says a container can notify the page about a changed segment but offers no way to ask for it. Tomcat’s Jasper does: editing |
Size |
Everything ends up in one |
Where the file lives |
A |
The duplicate-declaration failure is what the page static-twice.jsp (two identical includes of header.jspf) produced:
An error occurred at line: [3] in the jsp file: [/WEB-INF/jspf/header.jspf]
Duplicate local variable siteName
1: <%-- segment: no page directive; it becomes part of the including page --%>
2: <c:set var="site" value="Bookshelf" scope="request"/>
3: <% String siteName = "Bookshelf"; %>
The jsp:include Action (Dynamic Include)
<jsp:include page="urlSpec"/> does not paste text. At request time the container calls RequestDispatcher.include on the
target, which can be a JSP, a servlet or a static file, and the target’s output is written into the current out (Pages 4.0,
5.4). The target is a separate resource with its own page scope and its own translation unit, and the current page
carries on after it returns.
| Attribute | Meaning |
|---|---|
|
A relative URL specification, relative to the current JSP page, interpreted in the web application (so it is mapped: a servlet mapping or a JSP works as well as a file). Accepts a request-time value, so |
|
Optional. If |
The body may contain jsp:param elements that add request parameters for the duration of the inclusion (see the table that
follows). The Bookshelf shows the three things that surprise authors: what the fragment can see, what a fragment cannot do,
and how parameters merge.
<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
<div class="card">
<h3><c:out value="${param.title}"/> by <c:out value="${param.author}"/></h3>
<ul>
<li>title values: <c:forEach items="${paramValues.title}" var="t" varStatus="s"><c:out value="${t}"/><c:if test="${!s.last}"> | </c:if></c:forEach></li>
<li>dispatcher: ${pageContext.request.dispatcherType}</li>
<li>request attribute site: <c:out value="${site}" default="(none)"/></li>
<li>page variable book: <c:out value="${book.title}" default="(not visible)"/></li>
<li>include.request_uri: <c:out value="${requestScope['jakarta.servlet.include.request_uri']}"/></li>
</ul>
${pageContext.response.setHeader('X-From-Fragment', 'set')}${pageContext.response.setStatus(404)}
</div>
<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
<h1>Bookshelf</h1>
<c:forEach items="${books}" var="book">
<jsp:include page="/WEB-INF/jspf/book-card.jsp">
<jsp:param name="title" value="${book.title}"/>
<jsp:param name="author" value="${book.author}"/>
</jsp:include>
</c:forEach>
<p>after the includes: status=${pageContext.response.status} header=${pageContext.response.getHeader('X-From-Fragment')} title=<c:out value="${param.title}"/></p>
curl -s "http://localhost:18080/g3includes/v/dynamic-include?title=ORIGINAL"
<h1>Bookshelf</h1>
<div class="card">
<h3>Dune by Frank Herbert</h3>
<ul>
<li>title values: Dune | ORIGINAL</li>
<li>dispatcher: INCLUDE</li>
<li>request attribute site: Bookshelf</li>
<li>page variable book: (not visible)</li>
<li>include.request_uri: /g3includes/WEB-INF/jspf/book-card.jsp</li>
</ul>
</div>
<div class="card">
<h3>Neuromancer by William Gibson</h3>
...
<p>after the includes: status=200 header= title=ORIGINAL</p>
Read the output against the table of the previous section.
-
Parameters merge, new values first. The request carried
title=ORIGINAL; the action addedtitle=Dune. The fragment saw both, the new one first (Dune | ORIGINAL), as 5.6 requires, and after the include the page sees onlyORIGINAL: the new parameters live for the duration of the call. The container encodes the names and values, so the author writes them unencoded (awho=A%26B+Crequest parameter arrived intact asA&B Cthrough ajsp:param; observed). -
The fragment is a separate dispatch. Its dispatcher type is
INCLUDE, the page path methods still describe the including request, and the real path of the fragment is in thejakarta.servlet.include.request_uriattribute. -
Page scope is not shared. The loop variable
bookbelongs to the page scope of the including page, so the fragment printed(not visible). Request attributes (site,books) are visible, and anything else is passed as ajsp:param(aString) or as a request attribute (an object). -
The fragment cannot change the response. It called
setHeaderandsetStatus(404); the including page still saw status 200 and noX-From-Fragmentheader, and the final response was a plain 200. This is the rule of 5.4 and of the servlet include method.
Relative Paths: The Four A/B/C Cases
The specification gives four combinations of A.jsp, dir/B.jsp and two C.jsp files, to show that the directive is
resolved relative to the file and the action relative to the page. The scratch application builds them exactly, with
A in rel/, B1.jsp including C.jsp with the directive, B2.jsp with the action, and one C.jsp in rel/ and one in
rel/dir/ that print their own paths:
| A includes B with … | B includes C with … | C.jsp resolved to |
|---|---|---|
directive |
directive |
|
action |
action |
|
action |
directive |
|
directive |
action |
|
The fourth row is the one that bites: moving a fragment that contains jsp:include page="C.jsp" from a statically included
position to a dynamically included position changes what C.jsp means. Context-relative paths that start with / avoid
the question and are what the Bookshelf uses.
The flush Attribute and the Committed Response
flush="false" is the default, so the buffer stays in the page and the page can still forward, redirect or change headers
after the include. flush="true" commits the response before the include, with the consequences listed on the Servlet
Fundamentals page (no more status, headers or forward). The page below reports the state of the response after each form:
<%@ page contentType="text/plain;charset=UTF-8" trimDirectiveWhitespaces="true" session="false" %>
before: committed=${pageContext.response.committed}
<jsp:include page="/fragments/news"/>
after include flush=false: committed=${pageContext.response.committed}
<jsp:include page="/fragments/news" flush="true"/>
after include flush=true: committed=${pageContext.response.committed}
before: committed=false
news: dispatcher=INCLUDE books-attribute=true session-cookie=false lang=null
after include flush=false: committed=false
news: dispatcher=INCLUDE books-attribute=true session-cookie=false lang=null
after include flush=true: committed=true
A jsp:include of a resource that does not exist is not a 404 for the page: Tomcat 11.0.26 raised an exception from the
including page and answered 500 (observed with <jsp:include page="/fragments/missing"/>). Catching it needs c:catch
or an error page (the error handling page of this part).
The jsp:forward Action
<jsp:forward page="urlSpec"/> dispatches the current request to a static resource, a JSP or a servlet of the same web
application and effectively terminates the current page (Pages 4.0, 5.5). If the output is buffered, the buffer is cleared
before the forward, so text written earlier by the page is discarded. The action accepts jsp:param children with the
same merging rules as the include.
<%-- WEB-INF/views/forward.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" %>
this text is discarded by the forward
<jsp:forward page="/WEB-INF/views/forward-target.jsp">
<jsp:param name="from" value="forward.jsp"/>
</jsp:forward>
this text is never written
<%-- WEB-INF/views/forward-target.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" %>
target: dispatcher=${pageContext.request.dispatcherType} uri=${pageContext.request.requestURI}
from=${param.from} forwarded-from=${requestScope['jakarta.servlet.forward.request_uri']}
books=${books.size()}
curl -s http://localhost:18080/g3includes/v/forward
target: dispatcher=FORWARD uri=/g3includes/WEB-INF/views/forward-target.jsp
from=forward.jsp forwarded-from=/g3includes/v/forward
books=2
Neither the discarded first line nor the line after the action reached the client; the request attributes (books) survived;
the original URL of the request is in jakarta.servlet.forward.request_uri while the path methods describe the target.
If the buffer was already flushed, the forward fails. The specification says the attempt "will result in an
IllegalStateException`". On Tomcat 11.0.26 a page that wrote text, called `out.flush() and then jsp:forward produced a
200 response with the committed text only (committed output), and the server log recorded
java.io.IOException: Error: Attempt to clear a buffer that's already been flushed, thrown from
PageContextImpl.forward (observed with forward-flushed.jsp). Either way the client gets a broken page, so the rule is
the same: decide to forward before writing, and do not mix flush, autoFlush with a small buffer, or jsp:include
flush="true" with a later forward. A page that always forwards is a controller in disguise, and the right place for that
decision is a servlet (the Model 2 pattern below).
Forward Versus Redirect
A forward is internal to the container: one request, one response, the URL in the browser does not change, request
attributes survive. A redirect is an instruction to the browser: a 3xx status and a second request to a new URL, so
request attributes are lost.
Forward (jsp:forward, RequestDispatcher.forward) |
Redirect (c:redirect, sendRedirect) |
|
|---|---|---|
Visible to the browser |
No: same URL, same response |
Yes: a |
Request attributes |
Survive |
Lost: use the session, a flash value or the URL |
Reaches |
Only resources of the same web application, including |
Any URL, other applications and other sites included |
Use after a state-changing |
No: a refresh repeats the |
Yes: Post/Redirect/Get answers with a |
Typical use |
Controller to view |
After saving a book: redirect to the list |
c:redirect (Tags 7.6) sends the redirect and aborts the page. A URL starting with / gets the context path prepended,
exactly as c:url does, and c:param children are URL-encoded into the query string:
<%@ page session="false" %>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
<c:set var="book" value="${{'id': 1}}"/>
<c:redirect url="/books">
<c:param name="saved" value="${book.id}"/>
<c:param name="note" value="a b&c"/>
</c:redirect>
curl -s -i http://localhost:18080/g3includes/r.jsp
HTTP/1.1 302
Location: /g3includes/books?saved=1¬e=a+b%26c
Source: Jakarta Tags 3.0 — 7.7 c:param
The servlet-level equivalents (sendRedirect and its Servlet 6.1 overloads, status 303, relative Location headers) are in
Servlet Fundamentals. Only the controller should redirect after a POST; a
view that redirects has already produced part of the response.
The c:import Action
c:import (Tags 7.4) was designed for what jsp:include cannot do: reach a resource outside the web application, keep the
content in a variable, and avoid the extra buffering of a body that another action would then re-read. The url
attribute is one of three kinds (Tags 7.2.1): an absolute URL, a relative URL in the same context, or a relative URL in a
foreign context (with the context attribute).
| Attribute | Dynamic | Meaning |
|---|---|---|
|
yes |
The resource to import |
|
yes |
The context name (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. Normally needed only for absolute non-HTTP URLs |
The scratch application imports a servlet, /fragments/news, in all four ways. The servlet prints which dispatcher type reached
it, whether the request attributes and the cookie header arrived, and the lang parameter:
@WebServlet("/fragments/news")
public class NewsServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("news: dispatcher=" + req.getDispatcherType()
+ " books-attribute=" + (req.getAttribute("books") != null)
+ " session-cookie=" + (req.getHeader("Cookie") != null)
+ " lang=" + req.getParameter("lang"));
if ("bad".equals(req.getParameter("mode"))) {
resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
}
}
var and varReader (WEB-INF/views/import.jsp, first part)<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
<c:set var="base" value="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}"/>
<h2>relative</h2>
<c:import url="/fragments/news">
<c:param name="lang" value="es"/>
</c:import>
<h2>absolute</h2>
<c:import url="${base}/fragments/news">
<c:param name="lang" value="es"/>
</c:import>
<h2>var</h2>
<c:import url="/fragments/news" var="news" charEncoding="UTF-8"/>
<pre><c:out value="${news}"/></pre>
<h2>varReader</h2>
<c:import url="/fragments/news" varReader="r">
<c:out value="${r != null}"/>
</c:import>
after: <c:out value="${r}" default="(reader not visible after the end tag)"/>
curl -s -b 'JSESSIONID=whatever' http://localhost:18080/g3includes/v/import
<h2>relative</h2>
news: dispatcher=INCLUDE books-attribute=true session-cookie=true lang=es
<h2>absolute</h2>
news: dispatcher=REQUEST books-attribute=false session-cookie=false lang=es
<h2>var</h2>
<pre>news: dispatcher=INCLUDE books-attribute=true session-cookie=false lang=null
</pre>
<h2>varReader</h2>
true
after: (reader not visible after the end tag)
Source: Jakarta Tags 3.0 — 7.4 c:import
The difference between the first two blocks is the whole point. A relative URL is dispatched with RequestDispatcher.include
(dispatcher INCLUDE), so the request attributes, the parameters of the importing page and the session cookie travel
with it. An absolute URL is fetched with java.net.URLConnection as a brand-new GET (dispatcher REQUEST): "none of the
current execution environment" is available, even when the URL points at the same host and context, so the cookie, the
attributes and the parameters of the page are not propagated (7.4, "Absolute URL"). The c:param values do reach both,
in the query string. varReader is nested: the Reader exists only inside the body of the action, and c:param children
are illegal with it, because the connection is opened at the start tag (7.4).
Errors, Status Codes and What c:import Can Read
The second half of import.jsp wraps four failing imports in c:catch:
<h2>errors</h2>
<c:catch var="e1"><c:import url="${base}/fragments/news?mode=bad"/></c:catch>
<p>absolute 503: <c:out value="${e1.getClass().getSimpleName()}: ${e1.message}"/></p>
<c:catch var="e2"><c:import url="/fragments/missing"/></c:catch>
<p>relative missing: <c:out value="${e2.getClass().getSimpleName()}: ${e2.message}"/></p>
<c:catch var="e3"><c:import url="${base}/WEB-INF/web.xml"/></c:catch>
<p>absolute WEB-INF: <c:out value="${e3.getClass().getSimpleName()}: ${e3.message}"/></p>
<c:catch var="e4"><c:import url="/WEB-INF/web.xml" var="descr"/></c:catch>
<p>relative WEB-INF: <c:out value="${e4 == null ? 'read ' : e4.message}"/><c:out value="${fn:length(descr)}"/> characters</p>
<p>absolute 503: JspException: Problem accessing the absolute URL "http://localhost:18080/g3includes/fragments/news?mode=bad". java.io.IOException: Server returned HTTP response code: 503 for URL: http://localhost:18080/g3includes/fragments/news?mode=bad</p>
<p>relative missing: JspException: java.io.FileNotFoundException: The requested resource [/g3includes/fragments/missing] is not available</p>
<p>absolute WEB-INF: JspException: Problem accessing the absolute URL "http://localhost:18080/g3includes/WEB-INF/web.xml". java.io.FileNotFoundException: http://localhost:18080/g3includes/WEB-INF/web.xml</p>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" ...
<p>relative WEB-INF: read 0 characters</p>
Four lessons come from this run. First, a failing import throws a JspException (an unreachable or non-2xx absolute URL, a
relative resource that cannot be found), so wrap optional imports in c:catch. Second, a relative import dispatches inside
the container: it cannot be stopped by the HTTP layer that hides WEB-INF from browsers, and the descriptor of the
application was written into the page (its content went to the output and the var stayed empty; observed). The absolute
form went through HTTP and got a 404, as a browser would. Third, with a relative import the imported JSP’s own
setStatus(404) was turned into an exception (JspTagException: 404 /WEB-INF/jspf/book-card.jsp, observed with the
book-card.jsp of the previous section), while sendError(503) inside the imported servlet raised nothing (observed): do
not rely on status codes of imported resources, and treat the imported text as untrusted if it came from outside. Fourth,
the ${e1.getClass().getSimpleName()} form is deliberate: the .class property of old tutorials is not accepted by EL 6.0
on Tomcat 11.0.26 (the page fails to compile), so call getClass() as a method.
Security: Never Build the URL From Request Data
Because c:import and jsp:include accept expressions, <c:import url="${param.url}"/> and
<jsp:include page="${param.p}"/> are the JSP versions of server-side request forgery and of path traversal. With the
include, the scratch page unsafe.jsp (<jsp:include page="${param.p}"/>) answered a request for /WEB-INF/web.xml with
the application’s deployment descriptor (observed) and a request for a JSP under WEB-INF by executing it. With an
absolute c:import, the container’s own network position becomes the attacker’s: internal hosts, cloud metadata
endpoints, loopback services and file: or ftp: URLs are all reachable from the page. The OWASP SSRF prevention
cheat sheet lists the defences.
The safe form maps a short key to a fixed target; anything else is rejected. In the Bookshelf this is the feed.jsp view:
<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" session="false" %>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
<c:set var="feeds" value="${{'news': '/fragments/news'}}"/>
<c:set var="target" value="${feeds[param.feed]}"/>
<c:choose>
<c:when test="${not empty target}"><c:import url="${target}"/></c:when>
<c:otherwise>unknown feed: <c:out value="${param.feed}"/></c:otherwise>
</c:choose>
curl -s "http://localhost:18080/g3includes/v/feed?feed=news&lang=es"
curl -s "http://localhost:18080/g3includes/v/feed?feed=http://localhost:18080/"
news: dispatcher=INCLUDE books-attribute=true session-cookie=false lang=es
unknown feed: http://localhost:18080/
The same rule applies to jsp:forward and c:redirect: a redirect to a URL taken from a parameter is an open redirect.
Also output imported text with c:out when it is plain text, since an imported string kept in var is not escaped when it
is later written with ${news} in template text.
Foreign Contexts and the Related Tags
A relative URL in another web application is imported with context="/other" and a context-relative url. It uses the
RequestDispatcher of the other context, only the request environment of the importing page is available to it, and
it works only if the container allows cross-context dispatching (this was not exercised in the scratch application, which holds one web application): Tomcat needs crossContext="true" on the Context
element of the importing application, and the specification itself warns that "importing resources in foreign contexts may
not work in all containers". An absolute URL is the portable but slower alternative (Tags 7.4). c:url and the way the
URL tags rewrite, encode and prefix the context path are covered with c:param on the JSTL URLs page of this part;
the point needed here is that c:url and c:redirect prepend the context path to a URL starting with /, while
jsp:include and c:import of a relative URL take a path that is already relative to the application.
Source: Jakarta Tags 3.0 — 7.5 c:url
Choosing Between the Mechanisms
| Need | Use | Why |
|---|---|---|
Declare the tag libraries every page uses |
|
Resolved once at translation; shares tag prefixes |
Constants, one-line helpers, a banner text |
Static include (or a plain attribute) |
No dispatch cost, one translation unit |
Header, footer, navigation, a book card that loops and has its own logic |
|
Separate scope, independent compilation, parameters through |
Shared markup with parameters and a body |
A tag file (see the tag files page of this part) |
Typed attributes, |
Controller hands the request to a view |
|
One decision point before any output |
Content from another system, or text to post-process |
|
The only mechanism that reaches outside the application |
After a successful |
|
Post/Redirect/Get |
Page chosen from request data |
Never directly: map a key to a path |
|
Implicit Includes: include-prelude and include-coda
Many pages start with the same taglib directives and end with the same closing markup. Pages 4.0, 1.10.4 and 3.3.6 solve
this with two sub-elements of jsp-property-group in web.xml: include-prelude and include-coda name a
context-relative file that the container includes "as in an include directive" at the beginning or at the end of every page
matched by the group. The text is added at translation time to the same translation unit, so the page can use the prefixes
the prelude declares without declaring them again.
| Topic | Rule |
|---|---|
Value |
A context-relative path that must correspond to an element of the web application |
Several in one group |
Included in the order in which they appear |
Several groups match one page |
Applied in the order of the groups in |
Syntax |
Same as a static include: prelude and coda can use the other syntax, and start and end tags must be in the same file |
Pattern |
A |
Scope of the effect |
Every JSP file matching the group, including a page reached by |
<jsp-config>
<jsp-property-group>
<url-pattern>/WEB-INF/views/pages/*</url-pattern>
<include-prelude>/WEB-INF/jspf/prelude.jspf</include-prelude>
<include-coda>/WEB-INF/jspf/coda.jspf</include-coda>
</jsp-property-group>
</jsp-config>
<%-- WEB-INF/jspf/prelude.jspf --%>
<%@ taglib uri="jakarta.tags.core" prefix="c" %><%@ taglib uri="jakarta.tags.fmt" prefix="fmt" %>
<%-- WEB-INF/jspf/coda.jspf --%>
<!-- rendered by the Bookshelf prelude/coda -->
taglib at all<%-- WEB-INF/views/pages/home.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<h1>Books</h1>
<c:forEach items="${books}" var="b"><li><c:out value="${b.title}"/></li></c:forEach>
<%-- WEB-INF/views/pages/about.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" %>
<h1>About</h1>
<fmt:formatNumber value="1234.5" pattern="#,##0.00"/>
curl -s http://localhost:18080/g3includes/site/home
curl -s -H 'Accept-Language: en-US' http://localhost:18080/g3includes/site/about
<html><body>
<nav><a href="/g3includes/site/home">Home</a> | <a href="/g3includes/site/about">About</a></nav>
<h1>Books</h1>
<li>Dune</li><li>Neuromancer</li><!-- rendered by the Bookshelf prelude/coda -->
<hr>
</body></html>
Both pages used c: and fmt: tags without declaring them, and the coda comment appeared at the end of the page body
(before the layout’s closing markup, because the pages are included by layout.jsp). Three details from this run:
-
The
trimDirectiveWhitespacespage attribute matters here: on a page that does not set it, the lines of the directives and the newline at the end of the prelude became blank lines at the start of the output (observed). -
A page that repeats a taglib directive for the same prefix and URI is accepted; one that redefines the prefix
cto the functions library fails withAttempt to redefine the prefix [c] to [jakarta.tags.functions], when it was already defined as [jakarta.tags.core] in the current scope(observed with a prelude-covered page). -
fmt:formatNumberprinted1234.5and ignored itspatternwhen the request had noAccept-Languageheader and nofmt:setLocalehad run, and1,234.50with the header (observed). That is a formatting-locale question, not an include question, and it is covered on the JSTL formatting page of this part; the example above sends the header for that reason.
The group can also set other properties of the same pages (el-ignored, scripting-invalid, trim-directive-whitespaces,
page-encoding, default-content-type, buffer, and so on); they are listed on the configuration and property groups page of this part. A prelude is not a way to put logic in every page: it is meant for directives and markup.
Composition Patterns for the Bookshelf
Pattern 1: Header and Footer Through Includes
The simplest layout repeats <%@ include file="/WEB-INF/jspf/header.jspf" %> and the footer in every page, as in the first
example of this page. It works, but every page repeats the same two lines, and a change of the layout means editing every
page. A prelude and a coda remove the repetition for a group of pages; jsp:include removes the sharing of
variables.
Pattern 2: A Layout Page That Includes the Body
Invert the relationship: one layout.jsp owns the markup and includes the page body chosen by the controller. The
LayoutServlet maps a short name to a view, puts the model and the view path in request attributes and forwards to the layout,
which includes the body with page="${body}":
@WebServlet("/site/*")
public class LayoutServlet extends HttpServlet {
private static final Map<String, String> PAGES = Map.of(
"home", "/WEB-INF/views/pages/home.jsp",
"about", "/WEB-INF/views/pages/about.jsp");
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String body = PAGES.get(String.valueOf(req.getPathInfo()).substring(1));
if (body == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
req.setAttribute("books", List.of(
new Book(1, "Dune", "Frank Herbert", 1965),
new Book(2, "Neuromancer", "William Gibson", 1984)));
req.setAttribute("body", body);
req.getRequestDispatcher("/WEB-INF/views/layout.jsp").forward(req, resp);
}
}
<%@ page contentType="text/html;charset=UTF-8" trimDirectiveWhitespaces="true" session="false" %>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
<html><body>
<nav><a href="<c:url value='/site/home'/>">Home</a> | <a href="<c:url value='/site/about'/>">About</a></nav>
<jsp:include page="${body}"/>
<hr>
</body></html>
The output of /site/home and /site/about is the one shown in the implicit-includes section: the layout supplies the
navigation and the closing markup, each body supplies its own heading, and an unknown name such as /site/nope is a 404
from the controller (observed). The layout declares session="false" because it does not use the session: without
it, the first response would create a session and c:url would add ;jsessionid=... to the navigation links.
Pattern 3: Model 2, the Controller Forwards to the Layout
The two patterns above need a servlet that selects the view, and that is the Model 2 architecture introduced on the
introduction page: the controller reads the request, puts the model in request attributes, forwards to a JSP, and the JSP
renders without deciding anything. The forward is the hand-off; the includes are the composition inside the view. This is
also the shape of a Struts 2 action: the action fills the value stack and the dispatcher result forwards to a JSP; the
Struts include tag and the Tiles plugin are the Struts equivalents of jsp:include and of the layout above, and are
described on the Tags Overview & Generic Tags and Tiles & Page Layouts pages of this section.
Source: Apache Struts — include tag
Source: Apache Struts — Tiles plugin
Pattern 4: Tag Files and Custom Layout Tags
When a fragment needs typed parameters or a body (jsp:doBody), a tag file replaces the jsp:include plus jsp:param
pair, and a layout tag file with a jsp:invoke or jsp:doBody replaces the body attribute of Pattern 2. They are covered
on the tag files page of this part. Template engines and decorators outside plain JSP (Thymeleaf fragments, SiteMesh decorators, Tiles
definitions) are alternatives with their own composition model.
Pitfalls and Error Names
| Symptom or error | Cause | Fix |
|---|---|---|
|
A static include repeats a scriptlet declaration, or the same segment is included twice |
Do not declare variables in segments; use |
The fragment prints |
|
Pass a |
A header or status set in an included fragment never arrives (observed) |
Included resources cannot change the response |
Do it in the controller or in the including page |
|
The buffer was flushed (or |
Forward before writing; keep the buffer; decide in a servlet |
Text before a |
The buffer is cleared by the forward |
Expected: write nothing before a forward |
|
An exception in the including page |
Check the path; catch with |
|
The segment is outside |
Put segments under |
|
Request data used as a path |
Map a key to a path in the controller |
|
Request data used as a URL: SSRF |
Allowlist; never pass the parameter |
|
The imported JSP set a non-2xx status |
Do not set status in fragments; handle errors in the controller |
|
The resource is missing or failed |
Wrap optional imports in |
|
An absolute URL starts a new request, even to the same host |
Use a relative URL for resources of the same application |
|
EL 6.0 on Tomcat 11 does not accept the |
|
A static |
The attribute is a literal path, not an expression |
Use a literal path, or |
|
A prelude or page redefines a prefix to another URI |
Use one URI per prefix everywhere |
The prelude appears in every fragment too |
The group pattern is too broad |
Narrow the |
Relative include breaks after moving a fragment |
|
Use context-relative paths starting with |
Navigation links contain |
The page created a session and |
|
What Changed Recently
The include, forward and import mechanisms are among the most stable parts of the platform. The change log of Pages 4.0
(Appendix C.1, changes between 3.1 and 4.0) lists no change to jsp:include, jsp:forward, the include directive or the
implicit includes; the visible changes are around them.
| Change | Detail |
|---|---|
Pages 4.0 (Jakarta EE 11) |
No change to the include directive, |
Jakarta Tags 3.0 |
The tag-library URIs are |
Expression Language 6.0 |
The |
Servlet 6.1 |
Include and forward semantics are unchanged; Servlet 6.1 adds |
Tomcat 11 |
Jasper recompiles a page when a statically included file changes; that is documented in the Jasper HowTo and was verified here |
Namespace |
Every |
Where the Books Are Outdated
Web Development with JavaServer Pages and JSTL in Action (both Manning, listed in the section bibliography) cover the
same mechanisms: the include directive, jsp:include with flush, jsp:forward, jsp:param, and c:import for
including text, with channel and layout case studies built on them. The concepts survived; the surroundings did not. The
rows below describe the style of that era and of tutorials derived from it, not text of the books.
| The style of that era | Today (Pages 4.0, Tags 3.0, Tomcat 11) |
|---|---|
|
|
|
|
Shared headers kept as plain files in the web root |
Segments as |
Header and footer includes in every page |
A layout page that includes the body, or |
Scriptlet variables shared across a static include |
|
|
Context-relative paths |
|
Request attributes set by the controller, and EL ( |
|
The |
|
An allowlist; SSRF is a documented class of vulnerability |
|
Done in a controller or a service with a timeout, caching and error handling, then passed as a model; keep network calls out of views |
|
A controller servlet or a framework result; the forward action is for the view layer only |
|
|
Redirect after a |
Post/Redirect/Get with |
Tomcat 4 and Servlet 2.3 |
Tomcat 11.0.26 and Servlet 6.1 |
Source: Jakarta Tags 3.0 — 7.4 c:import