Building Responses
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.
Common content-type values you will use in practice:
text/html; charset=UTF-8— standard HTML responsesapplication/json— REST API responses (charset defaults to UTF-8 per RFC 8259)text/plain; charset=UTF-8— plain text, useful for diagnosticsapplication/xml— XML payloadsapplication/octet-stream— binary file downloads (combined withContent-Disposition)
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.
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.
The constants you will use most often — with their numeric equivalents for reference:
SC_OK(200) — successful response with a bodySC_CREATED(201) — POST created a resource; pair with aLocationheaderSC_NO_CONTENT(204) — success, no body (e.g., DELETE)SC_MOVED_PERMANENTLY(301) — permanent redirect; usesendRedirectfor 302SC_BAD_REQUEST(400) — client sent malformed dataSC_UNAUTHORIZED(401) — authentication requiredSC_FORBIDDEN(403) — authenticated but not permittedSC_NOT_FOUND(404) — resource does not existSC_INTERNAL_SERVER_ERROR(500) — unhandled server fault
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.
Practical Pattern: Building a JSON API Response
Bringing everything together, here is a realistic helper method many teams extract into a base servlet class:
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.
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.