Request & Response Handling Interview Questions & Answers
How Spring MVC binds and renders HTTP — HttpMessageConverters, @RequestBody/@ResponseBody, headers and cookies, file uploads with @RequestPart, CORS, interceptors vs filters, async DeferredResult, and setting status, headers, and the Location header.
HttpMessageConverters are the components that serialize and deserialize HTTP bodies —
they turn a request body into your object (@RequestBody) and your return value into a
response body (@ResponseBody).
// Jackson's MappingJackson2HttpMessageConverter handles both directions:
@PostMapping("/orders")
Order create(@RequestBody CreateOrderRequest req) { // JSON bytes → object (read)
return service.create(req); // object → JSON bytes (write)
}
Spring picks a converter by matching the request's Content-Type (for reading) or the
Accept header (for writing) against each converter's supported media types. Boot
auto-registers a set: Jackson for JSON, StringHttpMessageConverter for plain text,
ByteArrayHttpMessageConverter for binary, plus XML if the dependency is present.
Rule of thumb: HttpMessageConverters are the bridge between HTTP bytes and Java objects;
add a dependency (e.g. Jackson XML) to support a new media type rather than parsing bodies
yourself.
@RequestBody deserializes the incoming body into a parameter; @ResponseBody
serializes the return value into the response body. @RestController implies
@ResponseBody on every method.
@Controller // not @RestController here
class ApiController {
@PostMapping("/echo")
@ResponseBody // return value → response body (else: view name)
Message echo(@RequestBody Message in) { // request body → Message
return in;
}
}
Both delegate to HttpMessageConverters. On a @RestController you never write @ResponseBody
because it's baked in; you'd only spell it out on a plain @Controller that mixes view-rendering
and data-returning methods.
Rule of thumb: @RequestBody = body in, @ResponseBody = body out; @RestController
adds @ResponseBody for you.
The cleanest way is a ResponseEntity with a builder; for one-off cases you can inject the
raw HttpServletResponse.
@GetMapping("/report")
ResponseEntity<byte[]> download() {
byte[] pdf = service.render();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=report.pdf")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
ResponseEntity keeps header logic with the return value and is testable. For headers that
apply to many endpoints (security headers, etc.), set them once in a filter or
interceptor rather than repeating per handler.
Rule of thumb: Use ResponseEntity builders for per-response headers; push cross-cutting
headers into a filter/interceptor.
Accept a MultipartFile parameter (bound with @RequestParam or @RequestPart) on a
handler that consumes multipart/form-data.
@PostMapping(value = "/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity<String> upload(
@RequestPart("file") MultipartFile file, // the uploaded bytes
@RequestPart("meta") @Valid AvatarMeta meta) { // a JSON part, deserialized + validated
if (file.isEmpty()) return ResponseEntity.badRequest().build();
storage.save(file.getOriginalFilename(), file.getBytes());
return ResponseEntity.ok("stored");
}
@RequestPart is preferred over @RequestParam for mixed multipart bodies because it runs the
part through HttpMessageConverters (so a JSON part can be deserialized and validated). Tune
limits with spring.servlet.multipart.max-file-size / max-request-size.
Rule of thumb: Bind uploads to MultipartFile via @RequestPart, consume
multipart/form-data, and cap the size with spring.servlet.multipart.* properties.
The Location header tells the client the URI of a newly created resource. You set it
on a 201 Created response, conventionally with ResponseEntity.created(...).
@PostMapping("/orders")
ResponseEntity<Order> create(@RequestBody CreateOrderRequest req) {
Order saved = service.create(req);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest().path("/{id}") // /api/orders/{id}
.buildAndExpand(saved.id()).toUri();
return ResponseEntity.created(location).body(saved);
}
Building it from the current request avoids hard-coding the host/port. Location is also used
with 3xx redirects (302/303) to point at the next URL. Returning it on create lets
clients immediately GET the new resource.
Rule of thumb: On a 201, return a Location header pointing at the new resource; build it
with ServletUriComponentsBuilder so it's environment-correct.
CORS controls which browser origins may call your API. Configure it per-handler with
@CrossOrigin or globally with a WebMvcConfigurer.
@CrossOrigin(origins = "https://app.example.com") // per controller/method
@RestController
class OrderController { ... }
@Configuration
class CorsConfig implements WebMvcConfigurer { // global
@Override public void addCorsMappings(CorsRegistry r) {
r.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
Spring answers the browser's preflight OPTIONS request automatically once a mapping
exists. Avoid allowedOrigins("*") together with allowCredentials(true) — the browser
rejects that combination. With Spring Security present, also permit CORS at the security layer.
Rule of thumb: @CrossOrigin for a quick per-endpoint rule, a global WebMvcConfigurer
for the whole API; never combine wildcard origins with credentials.
Both wrap request processing, but at different layers. A Filter is a servlet-spec
component that sits outside Spring MVC and sees every request; a HandlerInterceptor
lives inside the DispatcherServlet and only wraps mapped handler methods, with access
to the resolved handler.
request → Filter(s) → DispatcherServlet → Interceptor.preHandle
→ @Controller method → Interceptor.postHandle → Interceptor.afterCompletion
→ Filter(s) → response
@Component
class TimingInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
req.setAttribute("start", System.nanoTime());
return true; // false short-circuits the request
}
}
Use a filter for low-level, MVC-agnostic concerns (request logging, compression, security
that must run before dispatch); use an interceptor when you need the matched handler or
ModelAndView, or want to limit to MVC routes.
Rule of thumb: Filter = servlet-level, sees everything, runs first; interceptor = MVC-level, knows the handler. Pick the layer that has the context you need.
Pass it as method parameters, store it on the HttpServletRequest as an attribute, or
use a request-scoped bean. Never put per-request data in controller fields.
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, // one instance per request
proxyMode = ScopedProxyMode.TARGET_CLASS) // proxy so singletons can inject it
class RequestContext {
private String correlationId;
// getters/setters — safe: each request gets its own instance
}
The proxyMode is essential: it injects a proxy into singleton beans that resolves to the
current request's instance on each call. Most of the time, though, plain method parameters or a
request attribute are simpler than a scoped bean.
Rule of thumb: Default to method parameters/request attributes; reach for a request-scoped bean (with a scoped proxy) only when many components need the same per-request value.
Return a Callable, DeferredResult, or CompletableFuture instead of the value
directly. Spring releases the servlet thread while the work runs and writes the response
when it completes.
@GetMapping("/quote")
CompletableFuture<Quote> quote(@RequestParam String symbol) {
return quoteService.fetchAsync(symbol); // container thread freed until this resolves
}
@GetMapping("/events")
DeferredResult<Event> waitForEvent() {
DeferredResult<Event> result = new DeferredResult<>(30_000L); // 30s timeout
eventBus.register(result::setResult); // completed later by another thread
return result;
}
Callable runs on a Spring-managed task executor; DeferredResult/CompletableFuture let
any thread complete the response later (long polling, external callbacks). The point is
throughput — the limited pool of servlet threads isn't blocked waiting on slow I/O.
Rule of thumb: Return CompletableFuture/DeferredResult for slow or event-driven work so
the servlet thread is freed; use Callable for offloading to a task executor.
Just declare them as method parameters — DispatcherServlet injects the current request and
response.
@GetMapping("/info")
String info(HttpServletRequest req, HttpServletResponse res) {
String ip = req.getRemoteAddr();
String ua = req.getHeader("User-Agent");
res.setHeader("X-Server", "node-1");
return ip + " / " + ua;
}
Reach for the raw objects only when the typed annotations (@RequestHeader, @RequestParam,
@CookieValue) don't cover what you need — e.g. inspecting the remote address or low-level
attributes. Prefer the annotations for normal binding because they're clearer and testable.
Rule of thumb: Accept HttpServletRequest/HttpServletResponse as parameters for raw
access, but prefer the typed binding annotations whenever they fit.
Return a StreamingResponseBody (or ResponseEntity<StreamingResponseBody>) and write to
the output stream incrementally, so the bytes aren't buffered in memory.
@GetMapping(value = "/export", produces = "text/csv")
ResponseEntity<StreamingResponseBody> export() {
StreamingResponseBody body = out -> {
try (var rows = repository.streamAll()) { // a lazy DB cursor
rows.forEach(r -> writeCsvLine(out, r)); // flush row by row
}
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=export.csv")
.body(body);
}
This keeps memory flat for huge exports. For server-pushed event streams there's
SseEmitter (Server-Sent Events); for chunked JSON there's ResponseBodyEmitter. All three
run the writing on an async thread so the servlet thread is freed.
Rule of thumb: Use StreamingResponseBody for big downloads, SseEmitter for server-sent
events — stream incrementally instead of buffering the whole payload.
In a REST controller, return a 3xx ResponseEntity with a Location header. In a plain
MVC @Controller, return a "redirect:/path" view name.
@GetMapping("/old-orders")
ResponseEntity<Void> moved() {
return ResponseEntity
.status(HttpStatus.MOVED_PERMANENTLY) // 301
.location(URI.create("/api/v2/orders"))
.build();
}
Use 301 for a permanent move, 302/303 for a temporary one (303 forces the client to
GET the new URL after a POST). For browser-form flows, the "redirect:" prefix also enables
RedirectAttributes flash attributes that survive the redirect.
Rule of thumb: REST → ResponseEntity with the right 3xx and a Location; server-rendered
MVC → "redirect:/path" view name.
Spring throws a typed exception and renders a default error response — 400 for bad
input, 415 for an unsupported Content-Type, 406 when Accept can't be satisfied,
405 for the wrong HTTP method.
malformed JSON body → HttpMessageNotReadableException → 400
@Valid body fails → MethodArgumentNotValidException → 400
missing required @RequestParam → MissingServletRequestParameterException → 400
wrong Content-Type → HttpMediaTypeNotSupportedException → 415
wrong HTTP method → HttpRequestMethodNotSupportedException → 405
Boot's default error handling wraps these in a JSON error body (timestamp, status, message).
You override the shape with @ExceptionHandler/@RestControllerAdvice or by extending
ResponseEntityExceptionHandler — the subject of the exception-handling page.
Rule of thumb: Binding failures map to sensible 4xx codes automatically; customize the
body with a @RestControllerAdvice, not by catching errors in each handler.
Spring sets the response Content-Type from the matched converter and the handler's
produces, defaulting JSON/text to UTF-8. You can pin it explicitly.
@GetMapping(value = "/greeting",
produces = MediaType.TEXT_PLAIN_VALUE + ";charset=UTF-8")
String greeting() { return "Café ☕"; } // bytes encoded as UTF-8
For JSON, Jackson writes UTF-8 by default, so non-ASCII just works. The servlet container's
default request encoding is also UTF-8 in Spring Boot (server.servlet.encoding.* to tune).
Mismatched encodings show up as mojibake (Café), almost always from a non-UTF-8 layer
upstream.
Rule of thumb: Boot defaults to UTF-8 end to end; only set charset explicitly when
serving text in a different encoding, and keep every layer on UTF-8 to avoid mojibake.
More Web MVC interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.