Skip to content

Request & Response Handling Interview Questions & Answers

15 questions Updated 2026-06-26 Share:

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.

Read the in-depth guideRequest and Response Handling in Spring MVC, Explained(opens in new tab)
15 of 15

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.

Read one with @CookieValue; write one by adding a Set-Cookie header, ideally via a ResponseCookie.

@GetMapping("/dashboard")
ResponseEntity<String> dashboard(
    @CookieValue(value = "session", required = false) String session) {   // read

    ResponseCookie cookie = ResponseCookie.from("theme", "dark")          // write
        .httpOnly(true).secure(true).path("/").maxAge(Duration.ofDays(30))
        .sameSite("Lax").build();
    return ResponseEntity.ok()
        .header(HttpHeaders.SET_COOKIE, cookie.toString())
        .body("ok");
}

ResponseCookie gives you the security attributes (HttpOnly, Secure, SameSite) you want on any real cookie. For absent optional cookies, mark required = false so you get null instead of a 400.

Rule of thumb: @CookieValue to read, ResponseCookie + Set-Cookie to write — and always set HttpOnly/Secure/SameSite on session cookies.

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel