Intermediate Reading time: ~9 min

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:

  1. Servlet Filters (earliest, servlet level)
  2. DispatcherServlet (Front Controller)
  3. HandlerInterceptor.preHandle() (handler level)
  4. Controller method execution
  5. HandlerInterceptor.postHandle() (before response)
  6. HandlerInterceptor.afterCompletion() (after response)
  7. 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:

  1. URL parameter: /api/users?format=xml
  2. Accept header: Accept: application/xml
  3. 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

  1. 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.

  2. How do you configure CORS in Spring? Globally: WebMvcConfigurer.addCorsMappings(). Controller-level: @CrossOrigin. Filter-level: CorsFilter bean. In production, always specify explicit origins.

  3. What is content negotiation? Based on the Accept header (and optionally URL parameters), Spring selects the appropriate HttpMessageConverter (JSON, XML). Customizable via configureContentNegotiation().

  4. How do you handle file uploads? @RequestPart MultipartFile file. Configuration: spring.servlet.multipart.max-file-size. Spring Boot automatically registers MultipartResolver.

  5. 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.

  6. What is OncePerRequestFilter? Guarantees the filter runs exactly once per request even with forward/include. Typical use: JWT token validation.

  7. 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