Directives

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 EE 11), Jakarta Expression Language 6.0, Jakarta Standard Tag Library 3.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 section 1.10 (Directives), section 3.3 (JSP Property Groups) and section 8.5 (Tag File Directives) of the Pages 4.0 specification, 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. A directive is a message to the JSP container: it is read once, when the page is translated into a servlet, it produces no output of its own, and it changes how the page is translated (which encoding to read the source in, which classes to import, which tag libraries to load, which files to paste in) and how the generated servlet behaves (buffering, sessions, the error page, the content type). There are three directives for JSP pages (page, include, taglib) and three more that only exist in tag files (tag, attribute, variable). The running scenario is the Bookshelf, a small catalogue of books, here as a handful of pages that each turn one directive on or off and show what changes in the HTTP response.

What a Directive Is

The syntax is <%@ directiveName attribute="value" ... %>, with optional white space after <%@ and before %>. Directives are evaluated at translation time, before any request is served, so their attribute values are literals: ${...} and scriptlet expressions are not allowed in them, and a directive can never depend on a request parameter. The page’s own pieces of template text, EL and actions are the request-time part (see JSP Lifecycle and Translation for the translation and request phases).

Table 1. The six directives of Jakarta Pages 4.0 and where each can appear
Directive Where Purpose Position and repetition

<%@ page %>

JSP pages (not tag files; a translation error in a tag file)

Page-level properties: language, imports, session, buffering, error page, content type, encoding, EL and whitespace switches

Position independent (except pageEncoding and contentType, which should come first). Several allowed; a repeated attribute must repeat the same value. import and pageEncoding may repeat

<%@ include %>

JSP pages and tag files

Translation-time inclusion of another file’s text

Wherever the file’s content should be pasted

<%@ taglib %>

JSP pages and tag files

Declares a tag library and binds a prefix

Must precede the first use of its prefix

<%@ tag %>

Tag files only (translation error in a JSP page)

The tag-file counterpart of page and of the TLD <tag> element

Position independent

<%@ attribute %>

Tag files only

Declares one attribute of the custom action

One per attribute name

<%@ variable %>

Tag files only

Declares a variable the tag exposes to the calling page

One per variable name

A directive that Jasper does not know is a hard error, not a warning. Verified on Tomcat 11.0.26: an unknown attribute (<%@ page colour="red" %>) fails the first request with [Page directive] has invalid attribute: [colour]. The translation unit is the page plus everything pasted in by static include directives and implicit includes, and all the page directives in that unit are merged into one set of attributes.

Most page attributes have a site-wide counterpart in web.xml (a jsp-property-group, described in Implicit Includes and JSP Property Groups and on Configuration and Property Groups), so a team can set a convention once and let a page override it only when it must. The sections below take the directives in turn, starting with the one that has the most attributes.

The page Directive

Table 2. Every attribute of the page directive in Jakarta Pages 4.0, with its default and its web.xml counterpart
Attribute Meaning Default jsp-property-group element

language

Scripting language of the scriptlets, expressions and declarations. java is the only value defined (lower case, case-sensitive). A different value must come before the first scripting element

java

none

extends

Fully qualified superclass of the generated servlet. Discouraged: it takes away the container’s freedom to supply a better superclass

container specific

none

import

Comma-separated types or package.* imports for the scripting and the EL environment. May repeat, cumulatively

java.lang.*, jakarta.servlet.*, jakarta.servlet.jsp.*, jakarta.servlet.http.* (implicit)

none (an include-prelude file can carry an import)

session

Whether the page takes part in an HTTP session. false makes the implicit session variable unavailable (a translation error if it is referenced)

true

none

buffer

none or a size in kilobytes with a mandatory kb suffix (16kb). The size is a lower bound

buffered, at least 8kb

buffer

autoFlush

true flushes a full buffer; false raises an exception on overflow. false together with buffer="none" is a translation error

true

none

info

A string returned by the generated servlet’s getServletInfo()

none

none

errorPage

Context-relative or page-relative URL to which an uncaught Throwable is forwarded. If the page sets it, the error-page elements of web.xml are not used for that exception

implementation dependent

none

isErrorPage

Marks this page as an error target. true defines the implicit exception variable; false makes referencing it a translation error

false

none

contentType

MIME type and optionally the response charset: TYPE or TYPE;charset=CHARSET. Also takes part in finding the page’s own encoding

text/html (text/xml for a JSP document)

default-content-type

pageEncoding

Character encoding of the JSP file itself (an IANA name). Not needed for JSP documents. At most once per file

the contentType charset, else ISO-8859-1

page-encoding

isELIgnored

true leaves ${...} and #{...} in the output untouched

false when web.xml is Servlet 2.4 or later; true for older descriptors

el-ignored

errorOnELNotFound

true throws PropertyNotFoundException for an identifier the EL machinery cannot resolve (added in Pages 3.1)

unspecified, which behaves as false (the identifier evaluates to null)

error-on-el-not-found

deferredSyntaxAllowedAsLiteral

true allows the character sequence #{ as plain text

false (translation error)

deferred-syntax-allowed-as-literal

trimDirectiveWhitespaces

true removes template text that consists only of white space. Ignored by JSP documents

false

trim-directive-whitespaces

The directive may appear any number of times in a translation unit, anywhere, with these rules, all enforced by Jasper:

  • A repeated attribute must repeat the same value. Verified: a second contentType with a different value fails with Page directive: illegal to have multiple occurrences of 'contentType' with different values (old: [text/html], new: [text/plain]).

  • import may repeat and accumulates in order; pageEncoding may repeat across files but occurs at most once per file.

  • pageEncoding and contentType should be at the top, because the container needs them to decide how to decode the rest of the file.

  • An unrecognized attribute or value is a fatal translation error, which includes isThreadSafe (see isThreadSafe, Removed in Pages 4.0).

A page that sets most of the attributes once is the best first example. The Bookshelf catalogue below sets the encoding, imports a class that EL can then use by its simple name, avoids creating a session, asks for a 16 KB buffer, adds an info string, trims directive white space and names an error page.

WEB-INF/views/catalogue.jsp, rendered by a servlet at /p/catalogue
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
         import="java.time.Year, org.example.bookshelf.Book"
         session="false" buffer="16kb" autoFlush="true"
         info="Bookshelf catalogue page" trimDirectiveWhitespaces="true"
         errorPage="/WEB-INF/views/error.jsp" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf</h1>
<p>Servlet info: ${pageContext.page.servletInfo}</p>
<p>Year: ${Year.now().value >= 2026}</p>
<ul>
<c:forEach var="b" items="${books}">
  <li>${b.title} (${b.year})</li>
</c:forEach>
</ul>
</body>
</html>

The servlet that supplies books and forwards to the page is the only Java the examples on this page need.

PageServlet.java: one servlet forwards /p/<name> to /WEB-INF/views/<name>.jsp
@WebServlet("/p/*")
public class PageServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getPathInfo().substring(1);
        req.setAttribute("books", List.of(
                new Book(1, "Dune", "Frank Herbert", 1965),
                new Book(2, "Neuromancer", "William Gibson", 1984)));
        req.getRequestDispatcher("/WEB-INF/views/" + name + ".jsp").forward(req, resp);
    }
}

Running curl -si http://localhost:18080/g3directives/p/catalogue on Tomcat 11.0.26 returned status 200 with no Set-Cookie header and this body:

Response of /p/catalogue
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf</h1>
<p>Servlet info: Bookshelf catalogue page</p>
<p>Year: true</p>
<ul>
<li>Dune (1965)</li>
<li>Neuromancer (1984)</li>
</ul>
</body>
</html>

Three things show in that output. The info string is the generated servlet’s getServletInfo(), reachable from EL through pageContext.page.servletInfo because the implicit page object is the servlet instance. The imported java.time.Year class is usable from EL as Year.now(), which is a Pages 3.1 clarification: the EL environment has the same default imports as the scripting environment. Finally the trimDirectiveWhitespaces attribute removed the blank lines that the two directive lines would otherwise leave at the top of the document (see trimDirectiveWhitespaces).

language, extends and info

language="java" is the default and the only value Pages 4.0 defines; naming another language fails on Tomcat with Page directive: invalid language attribute, verified with language="groovy". Writing it explicitly only documents the obvious, so most pages omit it.

extends names the superclass of the generated servlet. The superclass has to cooperate with the container’s generated code: verified, extends="java.lang.Object" fails translation with The method getServletConfig() is undefined for the type …​. The specification itself recommends against it, because it stops the container from choosing its own base class; adding objects to every page is better done with an implicit include (see Implicit Includes and JSP Property Groups) or, for scoped variables, with a servlet or filter that puts them in a scope.

info costs nothing, appears in getServletInfo() and so in management tooling, and is the safest attribute to learn what a page is. It is not secret, so it must not hold anything sensitive.

import and the Implicit Imports

The container always imports java.lang.*, jakarta.servlet.*, jakarta.servlet.jsp.* and jakarta.servlet.http.*, and nothing else. Any other class used in a scriptlet, or referenced by simple name in EL, needs an import, which may be a comma-separated list in one attribute or several directives:

Importing for EL, with two directives that repeat nothing
<%@ page import="java.time.Year" %>
<%@ page import="java.time.Month" contentType="text/plain" %>
<%@ page contentType="text/plain" %>
${Year.of(1965).isLeap()}-${Month.MARCH.value}

The output on Tomcat 11.0.26 is false-3. The two contentType attributes repeat the same value, which is allowed, and the two import directives add up. A static method or constant is reachable from EL only through a class, not an interface: verified, ${List.of(1)} with import="java.util.List" fails with The class [java.util.List] must be public, in an exported package, non-abstract and not an interface. When many pages need the same imports and tag libraries, put them in one file and include it with include-prelude (see Implicit Includes and JSP Property Groups).

session

Every JSP page takes part in a session by default, and the container creates the session eagerly when the page starts, whether or not the page uses it. Verified on Tomcat 11.0.26: a page that only prints text answers with Set-Cookie: JSESSIONID=...; Path=/g3directives; HttpOnly, and a page with session="false" does not:

A page that opts out of the session
<%@ page contentType="text/html; charset=UTF-8" session="false" %>
<p>no session</p>

On a public catalogue that no visitor logs in to, this attribute saves a cookie and a server-side session object per visitor. The price is that the implicit session variable is off limits on that page: <% out.print(session.getId()); %> fails with session cannot be resolved, a compile error reported against the generated servlet rather than a Jasper translation error. The attribute only controls the page’s own participation: a servlet that forwards to the page may still have created a session earlier. The servlet-level behaviour of sessions is covered in Servlet Fundamentals.

buffer and autoFlush

The out object is a JspWriter with an in-memory buffer, so the page can still change its mind (forward, set a header, send a redirect, hand over to an error page) until the buffer has been sent. The attribute takes none, or a size that must end in kb; buffer="8" fails with Page directive: invalid value for buffer. autoFlush decides what happens when the buffer fills: true (the default) sends it and keeps going, false throws. Verified results on Tomcat 11.0.26:

Table 3. Buffer and autoFlush combinations that were run
Page Result Consequence

buffer="none" autoFlush="false"

Translation fails; Tomcat 11.0.26 prints the unresolved message key jsp.error.page.badCombo

Without a buffer nothing can be held back, so autoFlush="false" makes no sense

buffer="1kb" autoFlush="false", 4000 characters written

java.io.IOException: Error: JSP Buffer overflow

Use when truncated output is worse than an error

buffer="1kb" (autoFlush true), 4000 characters written, then an exception

HTTP 200; the body is the part of the output that had already been flushed, followed by the error page’s markup

The response was already committed when the exception was thrown, so the status can no longer become 500

buffer="64kb", 4000 characters written, then an exception

HTTP 500 with the error page’s body and nothing of the earlier output

The buffer was discarded and the error page took over

The last two rows: the same output and the same failure, with only the buffer size different
<%@ page buffer="1kb" errorPage="/WEB-INF/views/error.jsp" contentType="text/plain" %>
<% for (int i = 0; i < 400; i++) { out.print("0123456789"); } %>
<% if (true) throw new IllegalStateException("late failure"); %>

The rule that follows is the one the specification states: once the buffer has been flushed, the dispatch of an uncaught exception to the error page may fail, because the status line and headers are on the wire. On Tomcat 11.0.26 the error page’s markup is appended to the half-sent page under a 200 status, which is worse than a clean 500. Pages that build large output (a CSV export, a report) should be written so that nothing that can fail happens after the first flush, or use a larger buffer, or compute the data in a servlet before the forward.

errorPage and isErrorPage

errorPage points an uncaught Throwable at another resource, and isErrorPage="true" on that resource turns on the implicit exception variable. The container stores the throwable in the request under both jakarta.servlet.jsp.jspException (backwards compatibility) and jakarta.servlet.error.exception (the Servlet name). From EL, pageContext.exception and pageContext.errorData (a jakarta.servlet.jsp.ErrorData) are the clean routes:

WEB-INF/views/error.jsp, the target of the errorPage attribute
<%@ page isErrorPage="true" contentType="text/html; charset=UTF-8" %>
<h1>Something went wrong</h1>
<p>message: ${pageContext.exception.message}</p>
<p>status ${pageContext.errorData.statusCode} for ${pageContext.errorData.requestURI}</p>
WEB-INF/views/boom.jsp, a page that fails on request
<%@ page errorPage="/WEB-INF/views/error.jsp" %>
<p>before</p>
<%
    if (request.getParameter("fail") != null) {
        throw new IllegalStateException("catalogue backend is down");
    }
%>
<p>after</p>

A request to /p/boom?fail=1 returned status 500 and this body (the earlier before line was in the buffer, so it was discarded):

Response of /p/boom?fail=1
<h1>Something went wrong</h1>
<p>message: catalogue backend is down</p>
<p>status 500 for /g3directives/WEB-INF/views/boom.jsp

Without isErrorPage="true" the target cannot see the exception: verified, <% out.print(exception); %> in an ordinary page fails with exception cannot be resolved to a variable. The error page should never print exception.printStackTrace or the message of an unknown exception to the visitor, which discloses internals; log the throwable and show a reference number instead. Note also that the attribute overrides web.xml: when a page names an errorPage, the error-page elements of the deployment descriptor are not consulted for exceptions that page throws. The whole mechanism, including the deployment-descriptor error pages and the ErrorData fields added in Pages 4.0 (query string and request method), is the subject of Error Handling.

contentType and pageEncoding

Two different encodings are involved and the two attributes are easy to confuse. pageEncoding says how the JSP file is stored: it is how the container turns the bytes of the .jsp into characters. contentType says what the response is: the MIME type and, through its charset, how the output characters are encoded for the client. For a JSP in standard syntax the file encoding is the pageEncoding charset if present, otherwise the contentType charset if present, otherwise ISO-8859-1.

flowchart TD start["Bytes of the JSP file
(standard syntax)"] --> pe{"pageEncoding
present?"} pe -->|"yes"| usePe["Decode the file
with that charset"] pe -->|"no"| ct{"contentType has
a charset?"} ct -->|"yes"| useCt["Decode the file
with that charset"] ct -->|"no"| iso["Decode the file
as ISO-8859-1"] usePe --> resp["Response encoding is separate:
charset of contentType if given,
else the container default"] useCt --> resp iso --> resp
Figure 1. How the encoding of the JSP file is chosen

The failing case is the third arm. A Bookshelf page saved as UTF-8 that contains an accented letter and declares neither attribute is decoded as ISO-8859-1, and the container then encodes the (wrong) characters again for the response. On Tomcat 11.0.26 the three variants gave:

Three pages containing the same UTF-8 text "Café Dune"
<%-- enc-nodecl.jsp: nothing declared --%>
<p>Café Dune</p>

<%-- enc-utf8.jsp: the contentType charset is also used to read the file --%>
<%@ page contentType="text/html; charset=UTF-8" %>
<p>Café Dune</p>

<%-- enc-pe.jsp: pageEncoding only --%>
<%@ page pageEncoding="UTF-8" %>
<p>Café Dune</p>

The first page produced the bytes C a f 303 203 302 251 (the two bytes of the UTF-8 letter, each re-encoded: Café in a browser). The second and third produced C a f 303 251 (correct). All three answered with a UTF-8 charset in the Content-Type header (Tomcat 11.0.26 defaults the response to UTF-8 when nothing is declared): only the file decoding is wrong. The fix is to put pageEncoding="UTF-8" (or a contentType with a charset) at the top of every JSP, or, once for the site, page-encoding in a jsp-property-group. The encoding of request parameters, forms and fmt is a separate topic covered on the character-encoding page of this section.

Non-HTML Content: contentType Beyond text/html

The default contentType is text/html (or text/xml for a JSP document), but the attribute accepts any MIME type, which makes a JSP a general text template: CSV, plain text, JSON-like text, dynamic CSS or JavaScript, XML feeds. The directive is the declarative way to set the response content type before any output is produced, and response.setContentType sets the same property from a scriptlet or a servlet (a call made after the response is committed has no effect, see Servlet Fundamentals). Dynamic CSS (text/css), JavaScript, plain text, XML and CSV are all served this way. A header such as Content-Disposition is not part of the directive and has to be set on the response by the forwarding servlet or a filter; the page below produces only the body. Detecting the client from User-Agent to pick a format, a technique older JSP texts spend a chapter on, is better replaced today by content negotiation (the Accept header) in the servlet.

WEB-INF/views/csv.jsp, a Bookshelf export (the two directives are on one line on purpose)
<%@ page contentType="text/csv; charset=UTF-8" trimDirectiveWhitespaces="true" %><%@ taglib prefix="c" uri="jakarta.tags.core" %>id,title,author
<c:forEach var="b" items="${books}">
${b.id},"${b.title}","${b.author}"
</c:forEach>
Response body of /p/csv, 75 bytes
id,title,author
1,"Dune","Frank Herbert"
2,"Neuromancer","William Gibson"

The response carried Content-Type: text/csv;charset=UTF-8, no <html> wrapper and no blank first line. Without trimming, the line break that follows an ordinary taglib line becomes the first bytes of the body (compare the 8-byte and 7-byte bodies in trimDirectiveWhitespaces), which breaks a CSV, an XML declaration that must start at byte 0 and anything else that is strict about its first character. CSV fields built from user text still need quoting and, for spreadsheets, protection against cells that start with an equals sign, plus, minus or at sign; c:out escapes XML, not CSV.

isELIgnored, errorOnELNotFound and deferredSyntaxAllowedAsLiteral

Three attributes decide how the page treats ${...} and #{...}. The expression language itself is covered on Expression Language Basics and the switches on EL Configuration & ELProcessor.

isELIgnored="true" passes EL through untouched, which is the right switch for a page that documents EL, generates a template for another engine or a JavaScript template literal, and is not written for the container’s EL. The default depends on the web.xml version: Servlet 2.4 and later descriptors evaluate EL; a Servlet 2.3 or older descriptor ignores it. The web-app of the Bookshelf is version 6.1, so EL is on. A tag file never inherits a default and always evaluates.

The page that ignores EL
<%@ page isELIgnored="true" contentType="text/plain" %>
price is ${1 + 2}

Run on Tomcat 11.0.26 the page answers with the text exactly as written, and no 3 appears:

price is ${1 + 2}

errorOnELNotFound is the new one (Pages 3.1). By default an identifier that no resolver can find, for example the mistyped ${noSuchName}, evaluates to null and prints as nothing, which hides misspelt attribute names. With errorOnELNotFound="true" the same expression throws jakarta.el.PropertyNotFoundException. The value can be given site-wide in web.xml with error-on-el-not-found, and the page attribute overrides it in either direction:

Table 4. How the two settings combine for JSP pages (Table JSP.3-4)
error-on-el-not-found in web.xml errorOnELNotFound in the page Unknown identifier

unspecified

unspecified

null is returned

false

unspecified

null is returned

true

unspecified

PropertyNotFoundException

any

false

null is returned

any

true

PropertyNotFoundException

Two pages that differ only in errorOnELNotFound
<%-- elmissing.jsp --%>
<%@ page errorOnELNotFound="true" contentType="text/plain" %>
Hello [${noSuchName}]

<%-- elmissing-false.jsp --%>
<%@ page contentType="text/plain" %>
Hello [${noSuchName}]

The second page answers Hello []. The first answers 500, and the root cause in the page shown is jakarta.el.PropertyNotFoundException: Unknown identifier [noSuchName]. Only an unresolvable identifier is affected: a missing property of an existing bean was already an error, and null in the middle of an expression still behaves as EL specifies. The setting is an easy way to catch typos in tests, and a development-site switch worth having; but it also turns a missing optional request attribute into a 500, so applying it blindly in production changes behaviour.

deferredSyntaxAllowedAsLiteral exists because #{ has been reserved since JSP 2.1 for deferred EL. By default the sequence in template text is a translation error. Verified:

A literal #{ in template text, with and without the attribute
<%-- hash.jsp: fails --%>
<%@ page contentType="text/plain" %>
literal: #{not an expression}

<%-- hash-ok.jsp: prints the text --%>
<%@ page deferredSyntaxAllowedAsLiteral="true" contentType="text/plain" %>
literal: #{not an expression}

The first page fails with #{...} is not allowed in template text (line 2, column 11), the second prints literal: #{not an expression}. Escaping the sequence (\#{) is the alternative and is covered on the syntax page of this section. The attribute exists mostly for pages written before JSP 2.1, for which Pages 3.4 defines backwards compatibility.

trimDirectiveWhitespaces

White space in template text is kept, so each directive on its own line leaves a line break behind. For a stylesheet, a CSV file or a fragment that is inserted into another page, those line breaks are noise. With trimDirectiveWhitespaces="true" any template text that consists only of white space is dropped; text that has any other character keeps all of its white space.

Two pages with the same directives, with trimming off and on
<%-- trim-off.jsp --%>
<%@ page contentType="text/plain" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
Hello

<%-- trim-on.jsp --%>
<%@ page contentType="text/plain" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
Hello

On Tomcat 11.0.26 the first body is 8 bytes (\n\n\nHello\n) and the second is 7 (\n\nHello\n). Only the whitespace-only text between the two directives was removed. The line break after the last directive is attached to Hello, so the text node is not whitespace-only and stays. That is why the CSV above puts its directives on one line: trimming does not remove the line break that leads into real content.

Tomcat also has a container-wide Jasper option, trimSpaces, with the values true, single, false (the default) and extended; a page or tag file that sets trimDirectiveWhitespaces to true takes precedence over it. The setting is ignored by JSP documents in XML syntax.

isThreadSafe, Removed in Pages 4.0

JSP 1.x and 2.x had a thirteenth attribute, isThreadSafe (default true). With false the page promised to be unsafe for concurrent access and the container had to serialize requests to it, which it did through the servlet SingleThreadModel. Servlet 6.0 removed SingleThreadModel, Pages 3.1 deprecated the attribute because of that, and Pages 4.0 removed it. The attribute is not in the 4.0 syntax list at all, so the attribute is a translation error today:

A page that still carries the old attribute
<%@ page isThreadSafe="false" %>
x

Verified on Tomcat 11.0.26: [Page directive] has invalid attribute: [isThreadSafe]. Old pages that carry it are upgraded by deleting it; the page is then handled by many threads at once, like every servlet. Whatever the attribute used to protect (instance fields declared in <%! %>, non-thread-safe helpers kept as fields) has to be fixed in the code: Scripting Elements (Legacy) shows why instance fields in a JSP are shared state and how to replace them with request attributes and EL.

The include Directive

<%@ include file="relativeURL" %> pastes the text of another file into the page while the page is translated. The included file becomes part of the same translation unit, so it is parsed as JSP, shares one generated servlet with the including page, and sees everything declared before it: tag library prefixes, imports, variables created by earlier actions. This is also why it is different from the request-time jsp:include action: the directive is a compile-time copy, the action is a call to another resource that returns its own output. The file attribute is a path relative to the including file (or context-relative when it starts with /), and it must be a literal: a request-time expression cannot be used, because the include is finished before any request.

Table 5. Summary of the two include mechanisms (Table JSP.1-10)
Syntax When Path is relative to The target is treated as

<%@ include file="..." %>

Translation time

the including file

Static text that the JSP container parses and merges

<jsp:include page="..." />

Request time

the current page

A resource that is called; its output is included

The Bookshelf shares a header and footer through .jspf fragments under /WEB-INF/jspf/:

WEB-INF/jspf/header.jspf and WEB-INF/jspf/coda.jspf
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<header>${"Bookshelf"}</header>
<footer>Bookshelf, 2 titles</footer>
WEB-INF/views/inc.jsp, which includes both through two different path forms
<%@ page contentType="text/plain" trimDirectiveWhitespaces="true" %>
<%@ include file="/WEB-INF/jspf/header.jspf" %>
<c:out value="${books[0].title}"/>
<%@ include file="../jspf/coda.jspf" %>

The page uses c:out without declaring the c prefix: the taglib directive that does it is inside header.jspf, which is part of the translation unit. The response is:

Response of /p/inc
<header>Bookshelf</header>
Dune<footer>Bookshelf, 2 titles</footer>

Both path forms worked, the context-relative /WEB-INF/jspf/header.jspf and the file-relative ../jspf/coda.jspf. A path that does not resolve fails at translation: JSP file [/WEB-INF/jspf/none.jspf] not found.

Three consequences are worth knowing:

  • The syntax of the included file is detected independently. A standard-syntax page can include a file in XML syntax and the reverse; the container parses each file by its own rules and requires an XML file to be well formed.

  • Because the file is pasted, the start tag and the end tag of a JSP action or custom action must be in the same file (<c:if> in the page and </c:if> in the fragment is rejected); plain HTML may span files.

  • A change to the included file only affects the including page after the page is translated again. The specification says a container may track included files for that and gives the author no way to declare the dependency. Check the Tomcat documentation for how Jasper detects it (the development and modificationTestInterval settings) before depending on it, and restart or touch the page after editing a fragment when in doubt.

Put included fragments under /WEB-INF/ so that a visitor cannot request them directly. The usual name .jspf is only a convention; what makes a file a JSP segment is that something includes it. The request-time alternative, jsp:include and c:import, with the forward-versus-redirect trade-offs, is the subject of Includes, Forwards & Page Composition.

The taglib Directive

<%@ taglib uri="..." prefix="..." %> or <%@ taglib tagdir="/WEB-INF/tags" prefix="..." %> makes a tag library available to the page and binds a prefix to it. The container resolves the identifier to a tag library descriptor (TLD): a uri is looked up among the TLDs in the application and its jars (a TLD declares its own <uri>), and a tagdir is a directory of tag files under /WEB-INF/tags, for which the container builds an implicit TLD (see Tag Library Descriptors & EL Functions and Tag Files & Layouts).

Table 6. Attributes of the taglib directive (Table JSP.1-9)
Attribute Rules

uri

An absolute or relative URI that identifies the TLD. Jakarta Tags 3.0 uses jakarta.tags.core, jakarta.tags.fmt, jakarta.tags.functions, jakarta.tags.xml and jakarta.tags.sql

tagdir

A directory of tag files, starting with /WEB-INF/tags; the directory must exist. Cannot be combined with uri

prefix

Required and non-empty; follows the XML namespace naming rules. The prefixes jsp:, jspx:, java:, jakarta: and servlet: are reserved

Run against Tomcat 11.0.26 with the JSTL 3.0.1 implementation, the five standard URIs resolved:

The five Jakarta Tags 3.0 identifiers
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="f" uri="jakarta.tags.functions" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="x" uri="jakarta.tags.xml" %>
<%@ taglib prefix="sql" uri="jakarta.tags.sql" %>
ok ${f:length("Dune")}

The page answered ok 4. A tagdir library is used in the same way (see the tag file example under Tag File Directives: tag, attribute and variable). Verified failures, each with the message Tomcat 11.0.26 printed:

  • an identifier no TLD declares: The absolute uri: [urn:example:nothing] cannot be resolved in either web.xml or the jar files deployed with this application

  • both uri and tagdir: Both 'uri' and 'tagdir' attributes specified

  • a tagdir outside the permitted directory: Tag file directory [/tags] does not start with "/WEB-INF/tags"

  • a reserved prefix, prefix="jsp": a Validation error messages from TagLibraryValidator message about the already declared xmlns:jsp

  • using a prefix before its taglib directive: The prefix [c] specified in this tag directive has been previously used by an action in file …​ line [1]

The last one matters when a page uses an action in a fragment that appears before the page’s own taglib line: declare the libraries in the prelude, in the first lines of the page, or in the same fragment that uses them. One more property is that a prefix, once bound, applies to the whole translation unit, and redefining it to a different URI in an included file is a translation error.

The http://java.sun.com/jsp/jstl/core style URIs still resolved on this setup (a page with uri="http://java.sun.com/jsp/jstl/core" printed its output), because the JSTL 3.0.1 implementation jar still ships legacy descriptors such as c-1_2.tld, but they are not the identifiers the Jakarta Tags 3.0 specification documents and a later implementation may drop them. Write jakarta.tags.* in new code and replace the old URIs when pages are migrated.

Implicit Includes and JSP Property Groups

Almost every JSP starts with the same few lines: the taglib directives and the imports. The jsp-config section of web.xml lets a group of pages (selected by a url-pattern) get those lines, and a number of other defaults, without repeating them. Each jsp-property-group can contain these elements:

Table 7. The elements of a jsp-property-group in Jakarta Pages 4.0 and the page attribute each replaces
Element Same effect as Notes

url-pattern

 — 

Required, can repeat. Same pattern rules as servlet mappings

el-ignored

isELIgnored

Group default; the page attribute overrides it

error-on-el-not-found

errorOnELNotFound

Pages 3.1 and later

page-encoding

pageEncoding

Encoding of the JSP file

scripting-invalid

 — (no page attribute)

true makes every scriptlet, declaration and expression a translation error

is-xml

 — 

Marks the matched files as JSP documents (XML syntax)

include-prelude

include directive at the top

Context-relative path; several allowed, in order

include-coda

include directive at the end

Same, for the end of the page

deferred-syntax-allowed-as-literal

deferredSyntaxAllowedAsLiteral

Default false

trim-directive-whitespaces

trimDirectiveWhitespaces

Default false

default-content-type

contentType

Used when the page has no contentType

buffer

buffer

Same values as the page attribute

error-on-undeclared-namespace

 — 

true makes an action with an undeclared namespace an error instead of ignoring it

The Bookshelf’s /WEB-INF/views/pg/ pages use one group:

web.xml: a jsp-property-group for the pg directory
<jsp-config>
    <jsp-property-group>
        <url-pattern>/WEB-INF/views/pg/*</url-pattern>
        <el-ignored>false</el-ignored>
        <error-on-el-not-found>true</error-on-el-not-found>
        <include-prelude>/WEB-INF/jspf/prelude.jspf</include-prelude>
        <include-coda>/WEB-INF/jspf/coda.jspf</include-coda>
        <trim-directive-whitespaces>true</trim-directive-whitespaces>
        <default-content-type>text/html;charset=UTF-8</default-content-type>
    </jsp-property-group>
</jsp-config>
WEB-INF/jspf/prelude.jspf
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ page import="org.example.bookshelf.Book" %>
WEB-INF/views/pg/list.jsp: no directive at all
<h1>Bookshelf</h1>
<c:forEach var="b" items="${books}">
  <p>${b.title} (${b.year})</p>
</c:forEach>

The page has no taglib and no contentType, and works. The response (status 200, Content-Type: text/html;charset=UTF-8) is:

Response of /p/pg/list
<h1>Bookshelf</h1>
<p>Dune (1965)</p>
<p>Neuromancer (1984)</p>
<footer>Bookshelf, 2 titles</footer>

Three further checks, all run: a page in that group with ${noSuchName} answers 500 with PropertyNotFoundException: Unknown identifier [noSuchName] (the group sets error-on-el-not-found), and the same page with <%@ page errorOnELNotFound="false" %> answers <p>Hello []</p> followed by the coda (the page overrides the group). The include-coda content comes after the page’s output. A common pitfall is the url-pattern: it follows the servlet mapping rules, so /WEB-INF/views/pg/*.jsp (a path prefix and an extension) is rejected when the application is deployed: Invalid <url-pattern> [/WEB-INF/views/pg/*.jsp] in servlet mapping. Use /WEB-INF/views/pg/* or *.jsp.

When more than one group matches a page, the prelude and coda elements of all matching groups apply, in the order the groups appear in web.xml; the "most specific wins" rule of other mappings does not apply here. Preludes and codas follow the rules of statically included files, so start and end tags must be balanced in each file.

The group also controls scripting. scripting-invalid is the lever for banning scriptlets from a code base: set it to true for *.jsp and any <% ... %>, <%! ... %> or <%= ... %> becomes a translation error, which makes the migration to EL, JSTL and tag files enforceable. The Bookshelf pages that were run for the error cases above use scriptlets deliberately and so would fail under that setting.

Tag File Directives: tag, attribute and variable

A tag file (.tag, or .tagx in XML syntax) under /WEB-INF/tags is a custom action written in JSP syntax (Tag Files & Layouts builds them in full). It is not a page, so some directives change: page is an error in a tag file (use tag), taglib and include work the same, and three directives are only valid in tag files. Verified both ways on Tomcat 11.0.26: a <%@ tag %> in a JSP page fails with [<%@ tag] directive can only be used in a tag file, and a <%@ page %> in a tag file fails with [<%@ page] directive cannot be used in a tag file.

Table 8. The tag directive: attributes that mirror TLD tag information and the page directive
Attribute Meaning and default

display-name

Short name for tools. Default: the tag file name without .tag

body-content

empty, tagdependent or scriptless. Default scriptless. The value JSP is a translation error

dynamic-attributes

Accepts attributes with names not declared. The value is the name of a page-scoped Map that holds them; the generated handler implements DynamicAttributes

small-icon, large-icon

Paths of icon images for tools

description, example

Documentation strings

language, import, pageEncoding

As in the page directive; pageEncoding is not allowed in .tagx files

isELIgnored, errorOnELNotFound, deferredSyntaxAllowedAsLiteral, trimDirectiveWhitespaces

As in the page directive, but without a web.xml counterpart

Table 9. The attribute directive: one per attribute the tag accepts
Attribute Meaning and default

name

Required and unique within the tag file

required

Default false

fragment

true means the attribute is a JspFragment evaluated by the tag; type and rtexprvalue must then be omitted. Default false

rtexprvalue

Whether the value may be a runtime expression. Unlike the TLD element, defaults to true

type

Runtime type of the value. Default java.lang.String. A primitive type is a translation error

description

Documentation string

deferredValue, deferredValueType, deferredMethod, deferredMethodSignature

Declare a deferred value or method expression attribute (for example for Faces); deferredValueType implies deferredValue, and a method signature implies deferredMethod

Table 10. The variable directive: a variable the tag exposes to its caller
Attribute Meaning and default

name-given

A constant variable name. Exactly one of name-given and name-from-attribute is required

name-from-attribute

The name comes from the value of another (translation-time, required, non-rtexprvalue, String) attribute; needs alias

alias

The local name that holds the value inside the tag file; the container synchronizes it with the caller’s variable

variable-class

Class of the variable. Default java.lang.String

declare

Whether the variable is declared in the caller after the tag. Default true

scope

AT_BEGIN, AT_END or NESTED. Default NESTED

description

Documentation string

The Bookshelf’s bookCard tag uses all three. It takes a required typed attribute, an optional fragment, a name given by an attribute, and a constant-name variable that survives the tag:

WEB-INF/tags/bookCard.tag
<%@ tag description="Renders one book" body-content="scriptless" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
<%@ attribute name="book" required="true" type="org.example.bookshelf.Book" description="The book to show" %>
<%@ attribute name="heading" fragment="true" %>
<%@ attribute name="varName" required="true" rtexprvalue="false" %>
<%@ variable name-from-attribute="varName" alias="label" variable-class="java.lang.String" scope="NESTED" %>
<%@ variable name-given="age" variable-class="java.lang.Long" scope="AT_END" declare="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="label" value="${book.title} by ${book.author}"/>
<c:set var="age" value="${2026 - book.year}"/>
<article><jsp:invoke fragment="heading"/>${label}<jsp:doBody/></article>
WEB-INF/views/tagdemo.jsp
<%@ page contentType="text/plain; charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="bs" tagdir="/WEB-INF/tags" %>
<c:forEach var="b" items="${books}" end="0">
<bs:bookCard book="${b}" varName="t"><jsp:attribute name="heading">[card]</jsp:attribute><jsp:body> /inner:${t}</jsp:body></bs:bookCard>
age=${age}
</c:forEach>

The response is the card, then age computed by the tag and visible to the page after the tag ended (AT_END):

Response of /p/tagdemo
<article>[card]Dune by Frank Herbert /inner:Dune by Frank Herbert</article>

age=61

The variable named by the caller (varName="t") was visible inside the tag body as t (NESTED), the value of the alias label was synchronized with it, and age was visible after the tag (AT_END). Two points. First, varName must be a literal string (rtexprvalue="false"): the container needs the name at translation time. Second, arithmetic in EL on a Long field such as 2026 - book.year produces a Long, so the first version of this tag, with variable-class="java.lang.Integer", failed on the first request with java.lang.ClassCastException: class java.lang.Long cannot be cast to class java.lang.Integer: the variable-class is a promise about the runtime type and the container casts to it. The tag mechanics themselves (fragments, jsp:invoke and jsp:doBody, variable synchronization, packaging as a JAR with a TLD) belong to the custom-tags pages of this section.

The XML Forms of the Directives

Each directive has an XML equivalent for JSP documents (.jspx): jsp:directive.page, jsp:directive.include, and, in tag files, jsp:directive.tag, jsp:directive.attribute and jsp:directive.variable. The taglib directive has no element form: in XML syntax a tag library is a namespace declaration on the root element (xmlns:c="jakarta.tags.core"). The attributes are the same as in the <%@ %> syntax.

The XML form of a page directive
<jsp:directive.page contentType="text/plain" session="false"/>
xml-form ok

The specification defines these elements for JSP documents. Tomcat 11.0.26 also accepted this page as a normal .jsp in standard syntax and honoured it (the response was Content-Type: text/plain;charset=UTF-8 with no session cookie), which is tolerated behaviour rather than a promise; keep the element for .jspx files and the <%@ %> form for .jsp. JSP documents, including is-xml and the other XML-specific rules, are covered on the jsp-documents-xml-syntax page of this section.

Security and Operations Notes

  • Errors are information. A page without errorPage and without a web.xml error-page shows the container’s own report, which on Tomcat 11.0.26 includes the source lines of the failing page and a stack trace (the runs above returned them). An error page declared once in web.xml, or per page with errorPage, avoids that. The error page must not echo exception messages from untrusted sources.

  • isELIgnored="true" is not a security feature. It stops EL from being evaluated on that page, which is useful when a template contains user-visible ${...} text, but it does nothing about output escaping. Output is still escaped with c:out or fn:escapeXml.

  • Do not let a path come from the request. The include directive cannot, because its file is a literal, but the jsp:include action and c:import accept expressions, and a page="${param.p}" is a file disclosure risk. The includes page of this section covers it.

  • scripting-invalid in a property group removes the whole class of scriptlet-based injection, and costs nothing for a code base that already uses EL and JSTL.

  • session="false" on public pages reduces the surface for session fixation and cookie handling, and reduces memory.

  • info, errorOnELNotFound and trimDirectiveWhitespaces carry no security risk in themselves, but a page that prints info may disclose the version of an internal component.

  • JSP files and fragments belong under /WEB-INF/ so the only way to reach them is through a servlet or a filter, which also keeps any errorPage configuration from being bypassed. A JSP in the web root can be requested directly with no data set up for it.

Pitfalls and Error Names

All the messages below are the ones Tomcat 11.0.26 returned in the runs on this page; the exception class is org.apache.jasper.JasperException for translation problems unless stated otherwise.

Table 11. Directive problems, their signature and the fix
Symptom or message Cause Fix

Mojibake such as Café and a correct Content-Type

No pageEncoding or charset in contentType: the file is read as ISO-8859-1

pageEncoding="UTF-8" on every page, or page-encoding in a property group

[Page directive] has invalid attribute: [isThreadSafe]

The attribute was removed in Pages 4.0

Delete it, and make shared state thread-safe

Page directive: illegal to have multiple occurrences of 'contentType' with different values

Two page directives (possibly in an included file) set different values

Keep one value; a prelude and the page must agree

Page directive: invalid value for buffer

No kb suffix

buffer="16kb"

jsp.error.page.badCombo

autoFlush="false" with buffer="none"

Choose a buffer, or remove autoFlush

HTTP 200 with half a page followed by the error page’s markup

The buffer was flushed before the exception

A larger buffer, less output before the risky code, or compute first in a servlet

java.io.IOException: Error: JSP Buffer overflow

Output bigger than the buffer with autoFlush="false"

autoFlush="true" or a bigger buffer

session cannot be resolved (compile error in the generated servlet)

session="false" and the page uses the implicit variable

Use session="true" for that page, or stop using a session there

exception cannot be resolved to a variable

The page is not marked isErrorPage="true"

Add isErrorPage="true" to the error target

#{...} is not allowed in template text

A literal #{ in text

deferredSyntaxAllowedAsLiteral="true" or escape the sequence

PropertyNotFoundException: Unknown identifier [x]

errorOnELNotFound is on and x is not a scoped variable

Fix the name or put it in a scope

Failed to parse the expression [${pageContext.exception.class.simpleName}] with The identifier [class] is not a valid Java identifier

class is no longer accepted as a property name in EL 6

Call getClass().getSimpleName(), or use ErrorData and the message

JSP file [/WEB-INF/jspf/none.jspf] not found

include path does not resolve (it is relative to the including file)

Use the context-relative path with a leading /

The absolute uri: [...] cannot be resolved in either web.xml or the jar files deployed with this application

No TLD declares that URI; the tag library jar is missing

Add the JSTL implementation dependency; check the jakarta.tags.* spelling

The prefix [c] specified in this tag directive has been previously used by an action

taglib after the first use

Move the taglib directive to the top or to a prelude

Both 'uri' and 'tagdir' attributes specified

The two forms were mixed in one directive

Two directives with two prefixes

Tag file directory [/tags] does not start with "/WEB-INF/tags"

tagdir outside the allowed root

Move tag files under /WEB-INF/tags

[<%@ tag] directive can only be used in a tag file / [<%@ page] directive cannot be used in a tag file

tag, attribute or variable in a page, or page in a tag file

Use page in pages and tag in tag files

java.lang.ClassCastException: class java.lang.Long cannot be cast to class java.lang.Integer

variable-class does not match the type EL produced

Declare java.lang.Long (or convert in the tag)

Invalid <url-pattern> [/WEB-INF/views/pg/*.jsp] in servlet mapping at deployment

A jsp-property-group pattern that mixes a path and an extension

/WEB-INF/views/pg/* or *.jsp

The method getServletConfig() is undefined for the type ...

extends names a class that is not a servlet base class

Remove extends

What Changed Recently

Table 12. Changes that touch directives, by release
Version Change

JSP 2.0

EL and isELIgnored; tag files with the tag, attribute and variable directives; jsp-property-group with el-ignored, page-encoding, scripting-invalid, include-prelude and include-coda

JSP 2.1

#{ reserved for deferred EL, and deferredSyntaxAllowedAsLiteral (Pages 3.3.8 and 3.4)

JSP 2.x, after the JSP 1.2 era of the books

trimDirectiveWhitespaces, and the group elements default-content-type, buffer and error-on-undeclared-namespace

JSP 3.0 (Jakarta EE 9)

Package rename javax.servlet.jsp to jakarta.servlet.jsp

JSP 3.1 (Jakarta EE 10)

errorOnELNotFound (page attribute, tag attribute and error-on-el-not-found group element); the EL environment gets the same default imports as scripting; isThreadSafe deprecated (because Servlet 6.0 removed SingleThreadModel); jsp:plugin deprecated

Pages 4.0 (Jakarta EE 11)

isThreadSafe removed; jsp:plugin and its related actions removed; deprecated EL classes in jakarta.servlet.jsp.el and JspException.getRootCause removed; ErrorData gets the query string and request method

Jakarta Tags 3.0

The tag library identifiers are jakarta.tags.core, jakarta.tags.fmt, jakarta.tags.functions, jakarta.tags.xml and jakarta.tags.sql

The specification document is still titled "Jakarta Server Pages" while the project page says "Jakarta Pages"; the introduction page of this section explains the naming, and this page uses the shorter current name.

Where the Books Are Outdated

The JSP book consulted for this section (a JSP 1.2 text) and the JSTL book (a JSTL 1.0 text) cover the same three directives, and the concepts carried over unchanged: directives are translation-time messages, the page attributes configure the generated servlet, include merges source while an include action calls another resource, and a taglib binds a prefix. What has moved since:

In the JSP 1.2 and JSTL 1.0 era Today Where it matters

isThreadSafe="false" as the way to make a page single-threaded

Removed in Pages 4.0; unrecognized attribute

Old pages fail to translate until it is deleted

Scriptlets that import javax.servlet.* classes

The implicit imports are jakarta.servlet.*, jakarta.servlet.jsp.* and jakarta.servlet.http.*

Scriptlet code that names javax.servlet.http.HttpSession no longer compiles

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

jakarta.tags.core; the old URIs are not documented in Jakarta Tags 3.0

Replace them while migrating, even though 3.0.1 still resolves them

No tag files, so no tag, attribute or variable directives: a custom tag needs a Java class and a TLD

Tag files replace most simple custom tags

A tag file is declared with three directives, not a Java class and a TLD

No isELIgnored, errorOnELNotFound, trimDirectiveWhitespaces or deferredSyntaxAllowedAsLiteral

All four exist, with group-level equivalents

Whitespace and EL behaviour can be tuned per site

File encoding left to the server

The specification’s fallback for the file is ISO-8859-1, while Tomcat 11.0.26 answered with a UTF-8 response charset

The mojibake case above is the mismatch between the two

Scripting-heavy pages with language="java" repeated everywhere

Scripting is legacy; scripting-invalid can forbid it

Teach EL, JSTL and tag files first

Non-HTML content (MIME types, CSV, dynamic CSS, code generation) through contentType plus response calls

Same directive; trimDirectiveWhitespaces and property-group default-content-type are new

Whitespace before the first byte of a CSV or XML document

The books are used here only to decide which topics deserve coverage; no text, listing or figure from them appears on this page.

References