HTTP Handling
request/response lifecycle, interceptors, filters, CORS, content negotiation
HTTP Handling
Spring HTTP handling covers the full request/response lifecycle: filters, interceptors, CORS, content negotiation, file uploads, caching, and error handling mechanisms.
1. Definition
HTTP handling in Spring encompasses the components that manage the entire request/response processing lifecycle — from the raw HTTP request arrival to the final response emission. This includes Servlet Filters, HandlerInterceptors, CORS configuration, content negotiation, file uploads, and HTTP cache management.
Spring Boot auto-configures the fundamental HTTP components, but understanding the fine-tuning is essential for production environments and interviews.
Client → Servlet Filter Chain → DispatcherServlet
→ HandlerInterceptor → Controller → Response
(CORS, Content-Type, Cache headers)
2. Core Concepts
HTTP request/response lifecycle
The complete processing order:
- Servlet Filters (earliest, servlet level)
- DispatcherServlet (Front Controller)
- HandlerInterceptor.preHandle() (handler level)
- Controller method execution
- HandlerInterceptor.postHandle() (before response)
- HandlerInterceptor.afterCompletion() (after response)
- Servlet Filters (response path back)
Servlet Filter
A Filter is the lowest-level HTTP processor — runs before and after the DispatcherServlet:
@Component
@Order(1)
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
long start = System.currentTimeMillis();
chain.doFilter(request, response); // pass through
long duration = System.currentTimeMillis() - start;
log.info("{} {} - {}ms",
req.getMethod(), req.getRequestURI(), duration);
}
}
HandlerInterceptor
Spring-level interceptor with access to the Spring context:
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("Authorization");
if (token == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false; // stops request processing
}
return true; // continues processing
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {
// After controller, before response commit
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// After response (cleanup, logging)
}
}
Registration:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
}
}
Filter vs Interceptor comparison
| Aspect | Servlet Filter | HandlerInterceptor |
|---|---|---|
| Level | Servlet (lowest) | Spring Handler |
| Spring context | Not guaranteed | Available |
| Lifecycle | doFilter() | preHandle/postHandle/afterCompletion |
| Registration | @Component / FilterRegistrationBean | WebMvcConfigurer |
| Ordering | @Order / FilterRegistrationBean.setOrder() | InterceptorRegistry.order() |
| Use cases | Security, encoding, compression | Auth, logging, tenancy |
3. Practical Usage
CORS configuration
Cross-Origin Resource Sharing (CORS) controls which domains can access the API:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600); // preflight cache 1 hour
}
}
Controller-level CORS:
@CrossOrigin(origins = "https://app.example.com")
@RestController
@RequestMapping("/api/users")
public class UserController { ... }
// Or at method level
@CrossOrigin(maxAge = 3600)
@GetMapping("/{id}")
public UserDto findById(@PathVariable Long id) { ... }
Content negotiation
Content negotiation determines the request/response format:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) {
configurer
.defaultContentType(MediaType.APPLICATION_JSON)
.favorParameter(true)
.parameterName("format")
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
}
Resolution strategies by priority:
- URL parameter:
/api/users?format=xml - Accept header:
Accept: application/xml - URL extension:
/api/users.xml(not recommended, deprecated)
File uploads (Multipart)
@RestController
@RequestMapping("/api/files")
public class FileController {
@PostMapping("/upload")
public ResponseEntity<String> upload(
@RequestPart("file") MultipartFile file,
@RequestPart("metadata") FileMetadata metadata) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("File is empty");
}
String filename = StringUtils.cleanPath(file.getOriginalFilename());
storageService.store(file, filename);
return ResponseEntity.ok("Uploaded: " + filename);
}
@PostMapping("/upload-multiple")
public ResponseEntity<String> uploadMultiple(
@RequestPart("files") List<MultipartFile> files) {
files.forEach(f -> storageService.store(f, f.getOriginalFilename()));
return ResponseEntity.ok("Uploaded " + files.size() + " files");
}
}
Configuration (application.properties):
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=50MB
spring.servlet.multipart.file-size-threshold=2KB
4. Code Examples
HTTP cache management
@GetMapping("/{id}")
public ResponseEntity<ProductDto> findById(@PathVariable Long id) {
ProductDto product = productService.findById(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)
.cachePublic()
.noTransform())
.eTag(product.version().toString())
.body(product);
}
// Conditional GET (If-None-Match)
@GetMapping("/{id}")
public ResponseEntity<ProductDto> findByIdConditional(
@PathVariable Long id,
WebRequest webRequest) {
ProductDto product = productService.findById(id);
String etag = product.version().toString();
if (webRequest.checkNotModified(etag)) {
return null; // 304 Not Modified
}
return ResponseEntity.ok()
.eTag(etag)
.body(product);
}
Custom FilterRegistrationBean
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
FilterRegistrationBean<RequestLoggingFilter> bean =
new FilterRegistrationBean<>();
bean.setFilter(new RequestLoggingFilter());
bean.addUrlPatterns("/api/*");
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
bean.setName("requestLoggingFilter");
return bean;
}
}
Request/Response wrapper
public class CachingRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public CachingRequestWrapper(HttpServletRequest request)
throws IOException {
super(request);
this.body = request.getInputStream().readAllBytes();
}
@Override
public ServletInputStream getInputStream() {
return new DelegatingServletInputStream(
new ByteArrayInputStream(body));
}
public String getBody() {
return new String(body, StandardCharsets.UTF_8);
}
}
ResponseBodyAdvice (response modification)
@RestControllerAdvice
public class ResponseWrapper implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
return new ApiResponse<>(true, body, null);
}
}
5. Trade-offs
Filter vs Interceptor vs AOP
| Aspect | Servlet Filter | HandlerInterceptor | AOP (@Aspect) |
|---|---|---|---|
| Level | Servlet | Spring Handler | Method |
| Access | Request/Response | Request/Response + Handler | Method args + return |
| Use case | Security, encoding | Auth, logging, timing | Transaction, audit |
| Timing | Earliest | After filters | Inside controller |
| Testing | MockFilterChain | MockMvc | Unit test |
CORS: Global vs Controller-level
| Approach | Advantage | Disadvantage |
|---|---|---|
| WebMvcConfigurer | Single location, easy to review | Not flexible per-endpoint |
| @CrossOrigin | Endpoint-level control | Scattered configuration |
| CorsFilter | Earliest, filter-level | Manual implementation |
Cache strategies
| Strategy | Use case |
|---|---|
| Cache-Control: max-age | Static content, rarely changing data |
| ETag + If-None-Match | Dynamic content, 304 Not Modified |
| Last-Modified | File-based content |
| no-cache | Always revalidate |
| no-store | Sensitive data, caching forbidden |
6. Common Mistakes
❌ Missing CORS configuration
Access to XMLHttpRequest at 'http://api.example.com'
from origin 'http://app.example.com' has been blocked by CORS policy
Always configure CORS when frontend and backend are on different domains!
❌ Incorrect filter ordering
// WRONG: security filter runs after logging
@Order(1) // LoggingFilter
@Order(2) // SecurityFilter → too late!
// CORRECT: security filter first
@Order(1) // SecurityFilter
@Order(2) // LoggingFilter
❌ Multipart limit too small
# Default is 1MB — throws "MaxUploadSizeExceededException" for large files
spring.servlet.multipart.max-file-size=1MB
# Production:
spring.servlet.multipart.max-file-size=50MB
spring.servlet.multipart.max-request-size=100MB
❌ Missing cache headers
Without Cache-Control, the browser uses its own heuristics — non-deterministic! Always specify explicit cache headers.
❌ Content-Type mismatch
// WRONG: client sends JSON but no @RequestBody
@PostMapping
public void create(CreateUserRequest req) { ... } // form data binding!
// CORRECT:
@PostMapping
public void create(@RequestBody CreateUserRequest req) { ... } // JSON
7. Deep Dive
OncePerRequestFilter
OncePerRequestFilter guarantees the filter runs exactly once per request — important for forward/include scenarios:
@Component
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtService.isValid(token)) {
SecurityContextHolder.getContext()
.setAuthentication(jwtService.getAuth(token));
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getServletPath().startsWith("/public");
}
}
RequestContextHolder
RequestContextHolder provides static access to the current request from any Spring bean:
public class AuditService {
public String getCurrentClientIp() {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder
.currentRequestAttributes();
return attrs.getRequest().getRemoteAddr();
}
}
Warning: works only in synchronous (Spring MVC) contexts. In WebFlux, use ServerWebExchange instead.
WebMvcConfigurer customization points
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) { }
@Override
public void addCorsMappings(CorsRegistry registry) { }
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) { }
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { }
@Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) { }
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> resolvers) { }
@Override
public void addReturnValueHandlers(
List<HandlerMethodReturnValueHandler> handlers) { }
}
Streaming response (large files)
@GetMapping("/download/{id}")
public ResponseEntity<StreamingResponseBody> download(
@PathVariable Long id) {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"export.csv\"")
.body(outputStream -> {
try (var cursor = dataService.streamResults(id)) {
while (cursor.hasNext()) {
outputStream.write(cursor.next().getBytes());
outputStream.flush();
}
}
});
}
8. Interview Questions
What is the difference between Servlet Filter and HandlerInterceptor? Filter: servlet level, runs earliest, doFilter(). Interceptor: Spring handler level, preHandle/postHandle/afterCompletion, has Spring context access.
How do you configure CORS in Spring? Globally:
WebMvcConfigurer.addCorsMappings(). Controller-level:@CrossOrigin. Filter-level:CorsFilterbean. In production, always specify explicit origins.What is content negotiation? Based on the Accept header (and optionally URL parameters), Spring selects the appropriate HttpMessageConverter (JSON, XML). Customizable via
configureContentNegotiation().How do you handle file uploads?
@RequestPart MultipartFile file. Configuration:spring.servlet.multipart.max-file-size. Spring Boot automatically registersMultipartResolver.What are Cache-Control and ETag? Cache-Control: instruction to browser/proxy (max-age, no-cache). ETag: content hash, enables Conditional GET returning 304 Not Modified.
What is OncePerRequestFilter? Guarantees the filter runs exactly once per request even with forward/include. Typical use: JWT token validation.
How would you test filters and interceptors? Integration test with MockMvc:
mockMvc.perform(get("/api/...")).andExpect(...). Filter:MockFilterChain. Interceptor:MockHttpServletRequest+ direct method call.
9. Glossary
| Term | Meaning |
|---|---|
| Servlet Filter | Lowest-level HTTP processor (doFilter) |
| HandlerInterceptor | Spring handler-level pre/post processing |
| CORS | Cross-Origin Resource Sharing, cross-domain access control |
| Content negotiation | Format selection based on Accept/Content-Type |
| MultipartFile | Wrapper object for uploaded files |
| Cache-Control | HTTP cache header (max-age, no-cache, no-store) |
| ETag | Entity Tag, content hash for conditional GET |
| OncePerRequestFilter | Runs exactly once per request (forward-safe) |
| RequestContextHolder | Static access to the current request |
| WebMvcConfigurer | Central customization interface (interceptor, CORS, converter) |
| FilterRegistrationBean | Filter registration in Spring Boot (URL pattern, order) |
| StreamingResponseBody | Streaming download for large files |
10. Cheatsheet
REQUEST LIFECYCLE:
Client → Servlet Filter → DispatcherServlet
→ Interceptor.preHandle() → Controller
→ Interceptor.postHandle() → Response
→ Interceptor.afterCompletion()
FILTER:
implements Filter / OncePerRequestFilter
@Component @Order(1)
FilterRegistrationBean (URL pattern, order)
INTERCEPTOR:
implements HandlerInterceptor
preHandle() / postHandle() / afterCompletion()
WebMvcConfigurer.addInterceptors()
CORS:
WebMvcConfigurer.addCorsMappings() Global
@CrossOrigin Controller/method
CorsFilter bean Filter level
CONTENT NEGOTIATION:
Accept header Client preference
Content-Type header Request body format
produces/consumes attribute Controller level
FILE UPLOAD:
@RequestPart MultipartFile
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=50MB
CACHE:
CacheControl.maxAge(1, HOURS) Cache duration
.eTag(version) Conditional GET
304 Not Modified Content unchanged
🎮 Games
10 questions