All articles
A Spring Boot RestController on embedded Tomcat verifying webhook signature headers from raw byte[] payload received through a local tunnel.
Spring BootJavawebhook debugginglocal testing

Spring Boot Webhooks: Raw Body, CSRF Exempt, Signature Headers

Spring Boot makes webhook endpoints straightforward with @RestController, but Spring Security CSRF and JSON message converters can block or mutate payloads before your handler runs. Local testing should prove raw byte[] access, CSRF exemptions on webhook paths, and signature verification against untouched bytes.

Create a @RestController webhook endpoint

Keep webhook controllers thin. Accept raw bytes, verify signatures, then deserialize:

@RestController
@RequestMapping("/webhooks")
public class StripeWebhookController {

    @PostMapping("/stripe")
    public ResponseEntity handle(
            @RequestBody byte[] payload,
            @RequestHeader("Stripe-Signature") String signature) {

        if (!verifier.isValid(payload, signature)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        JsonNode event = objectMapper.readTree(payload);
        handler.process(event);
        return ResponseEntity.ok().build();
    }
}

Using @RequestBody byte[] preserves the exact payload Tomcat received — ideal when providers sign raw JSON bodies.

Read the raw body when you need HttpServletRequest

Alternatively, read directly from the servlet request inside a filter or controller method:

byte[] body = request.getInputStream().readAllBytes();
String sig = request.getHeader("Stripe-Signature");

Avoid binding to a POJO or Map first — Jackson re-serialization changes whitespace and breaks HMAC checks. See the signature verification guide.

Disable CSRF for webhook paths only

Spring Security enables CSRF for browser sessions by default. Webhook POSTs fail with 403 unless you ignore CSRF for server-to-server routes:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/**"));
        return http.build();
    }
}

Scope the ignore pattern to /webhooks/** — do not disable CSRF globally.

Verify signature headers before business logic

Read provider headers such as Stripe-Signature or X-Hub-Signature-256, compute the expected digest from the raw byte array, and use a constant-time compare. Return 401 on mismatch, 200 quickly on success to reduce retries.

Local tunnel workflow

  1. Start the app: ./mvnw spring-boot:run or run from your IDE on port 8080.
  2. Expose embedded Tomcat: npx portpreview 8080.
  3. Paste the tunnel URL plus /webhooks/stripe into the provider dashboard.
  4. Send a test event and watch application logs for signature pass/fail.
  5. Replay the same event ID to confirm idempotent handling per retry patterns.

Common pitfalls

Filter order and body consumption

Filters that read HttpServletRequest.getInputStream() before your controller leave an empty stream. Use ContentCachingRequestWrapper if multiple layers must inspect the body.

Global @RequestBody binding

A global @ControllerAdvice or custom HttpMessageConverter that parses JSON early can alter bytes. Keep webhook endpoints on dedicated controllers with byte[] binding.

Slow handlers trigger retries

Providers retry on timeouts and non-2xx responses. Acknowledge fast, process async, and deduplicate by event ID.

Where to go deeper

For fundamentals, read what localhost tunneling is and how to debug webhooks locally. For signature mechanics, see the signature verification guide. For duplicate-safe handlers, read retry and idempotency patterns. start PortPreview free.

Frequently asked questions

Why do Spring Boot webhooks return 403 Forbidden?
Spring Security CSRF blocks POST requests without a session token. Add csrf.ignoringRequestMatchers for your webhook path, then verify provider signatures instead.
Should I use @RequestBody byte[] or a POJO for webhooks?
Use byte[] for signature verification. POJO binding re-serializes JSON and breaks HMAC checks against the original payload bytes.
How do I test Spring Boot webhooks locally?
Run spring-boot:run on port 8080, expose with npx portpreview 8080, paste the tunnel URL into the provider dashboard, and trigger test events.