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.

flowchart LR subgraph T["Translation time - once, when the page is compiled"] A["include directive: file attribute"] --> B["Text of the file is pasted into the page"] B --> C["One servlet class, one set of variables and tag prefixes"] end subgraph R["Request time - on every request"] D["<jsp:include page>"] --> E["RequestDispatcher.include: the target runs by itself and writes into the same output"] F["<c:import url>"] --> G{"Kind of URL"} G -->|"relative, same web application"| E G -->|"absolute"| H["New HTTP request: no request attributes, no session, no parameters"] end
Table 1. The three include mechanisms side by side (verified on Tomcat 11.0.26 unless a row says otherwise)
include directive jsp:include action c:import action

Syntax

<%@ include file="relativeURLspec" %>

<jsp:include page="urlSpec" flush="false"/>

<c:import url="url" var="x"/>

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 jsp:include), or the body of an HTTP response for an absolute URL

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 (page accepts a request-time value)

Yes (url is dynamic)

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 jsp:include. Absolute: nothing of the current request is passed

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 context

Result can be kept in a variable

No

No

Yes: var (a String) or varReader (a Reader)

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:

A header segment and a footer segment (WEB-INF/jspf/header.jspf and footer.jspf)
<%-- 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>
A page that includes both (WEB-INF/views/static-include.jsp)
<%@ 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

Table 2. What the specification and Tomcat do with a 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 c:forEach that the page closes. The same rule applies to preludes and codas

Same declaration twice

Including the same segment twice repeats its declarations. The scriptlet variable of header.jspf fails to compile: Duplicate local variable siteName (observed)

Tag prefix redefinition

A taglib directive that redefines a prefix already in scope is a translation error unless it redefines it to the same URI (1.10.5); an include of a segment that repeats taglib with the same URI is harmless (observed)

The file attribute is a literal

It is not an expression. <%@ include file="${param.f}" %> is not evaluated; Tomcat 11.0.26 answers with a confusing JasperException that mentions a null Jar, while a plain missing path gives JSP file [/WEB-INF/jspf/nothere.jspf] not found (observed)

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 header.jspf in an exploded web application changed the next response after the modification test interval (observed), and the Jasper HowTo lists "Recompile JSP when included page changes"

Size

Everything ends up in one _jspService method of one generated class (the footer printed the class name above), so many large includes make one large method. Move content that needs its own logic to jsp:include

Where the file lives

A .jspf outside WEB-INF is not a JSP page for the container: Tomcat served /exposed/x.jspf as text/plain source, scriptlets included (observed). Keep segments under WEB-INF

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.

Table 3. The jsp:include attributes (Pages 4.0, Table JSP.5-4)
Attribute Meaning

page

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 page="${body}" is legal

flush

Optional. If true and the page output is buffered, the buffer is flushed before the inclusion. The default is false

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.

A fragment that reports what it can see (WEB-INF/jspf/book-card.jsp)
<%@ 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>
The page that includes it once per book (WEB-INF/views/dynamic-include.jsp)
<%@ 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 added title=Dune. The fragment saw both, the new one first (Dune | ORIGINAL), as 5.6 requires, and after the include the page sees only ORIGINAL: the new parameters live for the duration of the call. The container encodes the names and values, so the author writes them unencoded (a who=A%26B+C request parameter arrived intact as A&B C through a jsp: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 the jakarta.servlet.include.request_uri attribute.

  • Page scope is not shared. The loop variable book belongs 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 a jsp:param (a String) or as a request attribute (an object).

  • The fragment cannot change the response. It called setHeader and setStatus(404); the including page still saw status 200 and no X-From-Fragment header, 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:

Table 4. The four combinations and the file that C.jsp resolved to (observed on Tomcat 11.0.26)
A includes B with …​ B includes C with …​ C.jsp resolved to

directive

directive

rel/dir/C.jsp

action

action

rel/dir/C.jsp

action

directive

rel/dir/C.jsp

directive

action

rel/C.jsp (the action is relative to the page A, because B’s text became part of A)

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.

A page that forwards and the target that receives the request
<%-- 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.

Table 5. Forward and redirect in a JSP application
Forward (jsp:forward, RequestDispatcher.forward) Redirect (c:redirect, sendRedirect)

Visible to the browser

No: same URL, same response

Yes: a 3xx response and a new request

Request attributes

Survive

Lost: use the session, a flash value or the URL

Reaches

Only resources of the same web application, including WEB-INF

Any URL, other applications and other sites included

Use after a state-changing POST

No: a refresh repeats the POST

Yes: Post/Redirect/Get answers with a 303 and a GET

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&note=a+b%26c

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).

Table 6. The c:import attributes (Tags 7.4)
Attribute Dynamic Meaning

url

yes

The resource to import

context

yes

The context name (starting with /) of a foreign context; the url must then start with /

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; visible only inside the body of the action

charEncoding

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:

A servlet that reports how it was reached (NewsServlet)
@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);
        }
    }
}
Relative, absolute, 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)

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:

Select the resource from an allowlist, never from the request (WEB-INF/views/feed.jsp)
<%@ 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.

Choosing Between the Mechanisms

Table 7. Which one to use in the Bookshelf
Need Use Why

Declare the tag libraries every page uses

include-prelude (or a static include of a taglib segment)

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

jsp:include

Separate scope, independent compilation, parameters through jsp:param

Shared markup with parameters and a body

A tag file (see the tag files page of this part)

Typed attributes, jsp:doBody, no scope leakage

Controller hands the request to a view

RequestDispatcher.forward in a servlet (or Struts result)

One decision point before any output

Content from another system, or text to post-process

c:import with an allowlisted URL, var

The only mechanism that reaches outside the application

After a successful POST

sendRedirect (or c:redirect)

Post/Redirect/Get

Page chosen from request data

Never directly: map a key to a path

jsp:include and c:import with request data leak WEB-INF and the network

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.

Table 8. The rules of include-prelude and include-coda (Pages 4.0, 3.3.6)
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 web.xml. This is the opposite of the way url-pattern conflicts are normally resolved for other elements, as the specification points out

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 url-pattern of the property group: an extension pattern (*.jsp) or a path pattern such as the specification’s /two/*

Scope of the effect

Every JSP file matching the group, including a page reached by jsp:include (the prelude’s taglib and the coda comment appeared in an included page; observed): a broad pattern adds the prelude to every fragment as well

web.xml: a prelude and a coda for the pages of the Bookshelf
<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>
The prelude and the coda
<%-- 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 -->
Two pages that declare no 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 trimDirectiveWhitespaces page 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 c to the functions library fails with Attempt 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:formatNumber printed 1234.5 and ignored its pattern when the request had no Accept-Language header and no fmt:setLocale had run, and 1,234.50 with 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}":

The controller maps names to bodies; it never uses the request text as a path (LayoutServlet)
@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);
    }
}
The layout (WEB-INF/views/layout.jsp)
<%@ 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.

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

Table 9. Include, forward and import pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Tomcat 11.0.26)
Symptom or error Cause Fix

Duplicate local variable while compiling (observed)

A static include repeats a scriptlet declaration, or the same segment is included twice

Do not declare variables in segments; use c:set or move the logic to the controller

The fragment prints (not visible) or an empty value for a loop variable (observed)

jsp:include is a separate dispatch with its own page scope

Pass a jsp:param or a request attribute

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

jsp:forward after out.flush() gives a truncated page, IOException in the log (observed)

The buffer was flushed (or flush="true" was used) before the forward

Forward before writing; keep the buffer; decide in a servlet

Text before a jsp:forward disappears

The buffer is cleared by the forward

Expected: write nothing before a forward

jsp:include of a missing file gives 500 (observed)

An exception in the including page

Check the path; catch with c:catch or an error page

.jspf source visible in the browser (observed)

The segment is outside WEB-INF and the default servlet serves it

Put segments under WEB-INF

${param.p} in jsp:include shows web.xml (observed)

Request data used as a path

Map a key to a path in the controller

c:import of a user URL contacts internal hosts

Request data used as a URL: SSRF

Allowlist; never pass the parameter

JspTagException: 404 /path from c:import (observed)

The imported JSP set a non-2xx status

Do not set status in fragments; handle errors in the controller

JspException ... FileNotFoundException or HTTP response code: 503 from c:import (observed)

The resource is missing or failed

Wrap optional imports in c:catch

c:import shows the wrong request data or no cookie (observed)

An absolute URL starts a new request, even to the same host

Use a relative URL for resources of the same application

${e.class.simpleName} fails to compile (observed)

EL 6.0 on Tomcat 11 does not accept the class property

${e.getClass().getSimpleName()}

A static file attribute with ${...} gives a Jar is null message (observed)

The attribute is a literal path, not an expression

Use a literal path, or jsp:include page="${...}" from an allowlist

Attempt to redefine the prefix [c] (observed)

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 url-pattern to the directory of full pages

Relative include breaks after moving a fragment

file is relative to the file, page to the page

Use context-relative paths starting with /

Navigation links contain ;jsessionid=... (observed)

The page created a session and c:url rewrote the links

session="false" for pages that do not use the session

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.

Table 10. What changed around page composition
Change Detail

Pages 4.0 (Jakarta EE 11)

No change to the include directive, jsp:include, jsp:forward, jsp:param or include-prelude/include-coda. Removals elsewhere: jsp:plugin and the isThreadSafe page attribute (see the standard actions page of this part)

Jakarta Tags 3.0

The tag-library URIs are jakarta.tags.*; the old http://java.sun.com/jsp/jstl/* URIs still resolved on Tomcat 11.0.26 with JSTL 3.0.1 (observed) but are not defined by the 3.0 specification. c:import, c:redirect, c:url and c:param are unchanged

Expression Language 6.0

The class property is rejected on Tomcat 11.0.26 (observed); calling getClass() as a method works

Servlet 6.1

Include and forward semantics are unchanged; Servlet 6.1 adds jakarta.servlet.error.query_string and jakarta.servlet.error.method to error dispatches (see Servlet Fundamentals)

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 javax.servlet import in old code becomes jakarta.servlet; the container does not accept both

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.

Table 11. Material in the books and its current equivalent
The style of that era Today (Pages 4.0, Tags 3.0, Tomcat 11)

javax.servlet.jsp and the http://java.sun.com/jsp/jstl/core URI

jakarta.servlet.jsp and jakarta.tags.core; the old URI still resolved here, but write the new one

<jsp:include page="x.jsp" flush="true"/> as the normal form

flush defaults to false; true commits the response and blocks a later forward

Shared headers kept as plain files in the web root

Segments as .jspf under WEB-INF; a file outside it is served as source

Header and footer includes in every page

A layout page that includes the body, or include-prelude/include-coda for a group

Scriptlet variables shared across a static include

c:set and request attributes; scriptlets are legacy

jsp:include with a relative path that depends on directory layout

Context-relative paths

jsp:param and request.getParameter as the only way to pass data

Request attributes set by the controller, and EL (${param.title})

jsp:include page built from a request parameter with a scriptlet expression

The file attribute of the directive cannot be an expression, and an unchecked request value in the action leaks WEB-INF

c:import of an arbitrary URL as a content-syndication tool

An allowlist; SSRF is a documented class of vulnerability

c:import of RSS or other channels to build a page

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

jsp:forward as the way to implement page flow

A controller servlet or a framework result; the forward action is for the view layer only

${exception.class.name} style expressions

.class is not accepted by EL 6.0 on Tomcat 11 (observed): call getClass()

Redirect after a POST with a plain 302 and no thought for refresh

Post/Redirect/Get with 303, c:redirect or sendRedirect

Tomcat 4 and Servlet 2.3

Tomcat 11.0.26 and Servlet 6.1