Java Web & Servlets Fundamentals

Building Responses

18 min Lesson 6 of 13

Building Responses

A servlet's job is to receive an HTTP request and manufacture a well-formed HTTP response. The previous lesson focused on dissecting the request; this lesson focuses on the other side of the exchange: writing the response body, setting the correct content type, controlling status codes, and adding response headers. Getting all four right is the difference between a servlet that works reliably across browsers, proxies, and API clients, and one that is brittle in production.

The HttpServletResponse Object

The container hands every service method a jakarta.servlet.http.HttpServletResponse. Through it you control every aspect of the HTTP response. The two most important decisions you must make before you write a single byte of the body are: the content type (which also sets the character encoding) and the status code. Both must be committed before the body or they will either be silently dropped or cause an IllegalStateException.

Setting the Content Type

Always call response.setContentType() before obtaining a writer or stream. The content type tells the browser (or API client) how to interpret the bytes you are about to send.

@WebServlet("/api/status") public class StatusServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // Must be called BEFORE getWriter() or getOutputStream() resp.setContentType("application/json"); resp.setCharacterEncoding("UTF-8"); PrintWriter out = resp.getWriter(); out.print("{\"status\":\"ok\",\"version\":\"1.0\"}"); } }

Common content-type values you will use in practice:

  • text/html; charset=UTF-8 — standard HTML responses
  • application/json — REST API responses (charset defaults to UTF-8 per RFC 8259)
  • text/plain; charset=UTF-8 — plain text, useful for diagnostics
  • application/xml — XML payloads
  • application/octet-stream — binary file downloads (combined with Content-Disposition)
Set both content type and character encoding explicitly. setContentType("text/html; charset=UTF-8") is shorthand for calling both setContentType("text/html") and setCharacterEncoding("UTF-8"). If you skip the charset, the container may default to ISO-8859-1, which silently corrupts any non-ASCII characters you write.

Writing the Response Body

You have two mutually exclusive options for writing the body: a character-based PrintWriter (for text) or a byte-based ServletOutputStream (for binary). Trying to obtain both from the same response throws an IllegalStateException — the container enforces this strictly.

// TEXT responses — use PrintWriter resp.setContentType("text/html; charset=UTF-8"); PrintWriter writer = resp.getWriter(); writer.println("<html><body><h1>Hello</h1></body></html>"); // Do NOT call writer.close() — let the container flush and close it // BINARY responses — use ServletOutputStream resp.setContentType("image/png"); ServletOutputStream out = resp.getOutputStream(); Files.copy(Path.of("/var/app/images/logo.png"), out); // Same rule: do not close the stream yourself
Never call close() on the writer or stream. Closing the response stream inside your servlet can prevent the container from appending any pending data (e.g., session cookie headers set after your code returns) and interferes with Keep-Alive connection management. Let the container manage stream lifecycle.

Setting the HTTP Status Code

The default status is 200 OK. For anything else, call response.setStatus(int) before writing the body. Use the named constants in HttpServletResponse instead of raw numbers — they document intent and reduce typos.

@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String id = req.getParameter("id"); Product product = catalog.findById(id); if (product == null) { // 404 — resource not found resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setContentType("application/json"); resp.getWriter().print("{\"error\":\"Product not found\"}"); return; } resp.setStatus(HttpServletResponse.SC_OK); // 200, also the default resp.setContentType("application/json"); resp.getWriter().print(product.toJson()); }

The constants you will use most often — with their numeric equivalents for reference:

  • SC_OK (200) — successful response with a body
  • SC_CREATED (201) — POST created a resource; pair with a Location header
  • SC_NO_CONTENT (204) — success, no body (e.g., DELETE)
  • SC_MOVED_PERMANENTLY (301) — permanent redirect; use sendRedirect for 302
  • SC_BAD_REQUEST (400) — client sent malformed data
  • SC_UNAUTHORIZED (401) — authentication required
  • SC_FORBIDDEN (403) — authenticated but not permitted
  • SC_NOT_FOUND (404) — resource does not exist
  • SC_INTERNAL_SERVER_ERROR (500) — unhandled server fault
sendError vs setStatus: resp.sendError(404, "Not found") sets the status code AND triggers the container's error page mechanism (i.e., the error-page mappings in web.xml or @WebServlet). setStatus just sets the code and lets you write your own body. For APIs, prefer setStatus + a JSON error body. For HTML applications, prefer sendError so the container renders the configured error pages.

Adding Response Headers

Beyond the status line and content type, HTTP responses carry headers that control caching, security, redirects, and more. Use response.setHeader(name, value) for a single value or addHeader(name, value) to append additional values for a header that allows multiples.

// Cache-Control: prevent sensitive responses from being cached resp.setHeader("Cache-Control", "no-store"); // Location header — mandatory when returning 201 Created resp.setStatus(HttpServletResponse.SC_CREATED); resp.setHeader("Location", "/api/products/" + newProduct.getId()); // Content-Disposition — trigger a file download in the browser resp.setContentType("application/pdf"); resp.setHeader("Content-Disposition", "attachment; filename=\"report.pdf\""); // CORS header — allow a specific origin to call this endpoint resp.setHeader("Access-Control-Allow-Origin", "https://app.example.com"); // Custom application header — e.g., API version resp.setHeader("X-API-Version", "2");

Practical Pattern: Building a JSON API Response

Bringing everything together, here is a realistic helper method many teams extract into a base servlet class:

protected void writeJson(HttpServletResponse resp, int status, String json) throws IOException { resp.setStatus(status); resp.setContentType("application/json"); resp.setCharacterEncoding("UTF-8"); resp.setHeader("Cache-Control", "no-store"); resp.getWriter().print(json); } // Usage in a handler method: writeJson(resp, HttpServletResponse.SC_OK, "{\"message\":\"Order placed\",\"orderId\":42}");

Centralising these four lines eliminates the risk of forgetting the charset or the Cache-Control header on any single endpoint.

Buffering and Committing the Response

The container buffers response output before sending it. As long as the buffer has not been flushed, you can still modify headers and status. Once the buffer is flushed — either because it filled up, you called flushBuffer(), or the response was committed — the headers are locked in. Any subsequent call to setStatus or setHeader will be silently ignored.

You can query and adjust the buffer size with resp.getBufferSize() and resp.setBufferSize(int). Increasing it slightly (e.g., to 16 KB) is useful when you need to decide the final status after doing some work but before committing, such as building a response and only then checking for errors.

Keep status-and-header logic at the top of your method. The safest habit is to compute all status codes and headers first, then obtain the writer and write the body. This way you are never in a position where the response has already committed when you discover you need to send a 404.

Summary

Building a correct HTTP response requires four things working together: the right content type (set before writing), the right character encoding (UTF-8 unless you have a specific reason otherwise), the right status code (use the SC_* constants), and any headers the client needs to interpret or cache the response correctly. Always set status and headers before writing the body, never close the writer yourself, and prefer centralised helpers over scattering these four lines across every handler. The next lesson combines everything from Lessons 5 and 6 to handle HTML forms with both GET and POST.