spring-webfluxwebtestclientbucket4j

Having multiple request IPs in Spring WebTestClient


I'm introducing Bucket4J in my Spring Web application. A basic test setup can be found here:

Bucket4J offers to rate limit on IP basis - so every IP gets its own pool of tokens. This can be done by adding expression: "getRemoteAddress()" to the config:

bucket4j:
enabled: true
filters:
  - metrics:
    types:
      - consumed-counter
      - rejected-counter
  - cache-name: buckets
    filter-method: webflux
    url: .*
    filter-order: 1
    rate-limits:
      - bandwidths:
          - capacity: 1
            time: 10
            unit: seconds
        expression: "getRemoteAddress()"

I'm having a hard time figuring out how to programmatically test if filter by IP is working.

The test for a single IP looks like this:

@ActiveProfiles("test")
@SpringBootTest
class RateLimitTest(
    @Autowired val context: ApplicationContext
) {

@Test
fun `FAILS with status code 429 if rate limit is exceeded`() {
    // arrange
    val client = WebTestClient
        .bindToApplicationContext(context)
        .configureClient()
        .build()

    // ac
    client.get()
        .uri("/api/someendpoint")
        .exchange()
        .expectStatus().isOk
        .expectHeader().valueEquals("X-Rate-Limit-Remaining", "0")

    client.get()
        .uri("/api/someendpoint")
        .exchange()
        .expectStatus().isEqualTo(HttpStatus.TOO_MANY_REQUESTS)
        .expectBody().jsonPath("error", "Too many requests!")

    // assert

}

What should a test look like that verifies behaviour for multiple IPs / IP rate limiting?
FAILS with status code 429 if rate limit * for IP * is exceeded


Solution

  • Actually my approach was wrong in general. The application in question is behind a load balancer so querying getRemoteAddress() will only give me the IP of the load balancer.

    I had to use the header X-FORWARDED-FOR and this made it super easy to create a test for it 😃

    bucket4j:
      enabled: true
      filters:
        - metrics:
            types:
              - consumed-counter
              - rejected-counter
        - cache-name: buckets
          filter-method: webflux
          url: ^(/api/someendpoint).*
          filter-order: 1
          rate-limits:
            - bandwidths:
                - capacity: 1
                  time: 10
                  unit: seconds
              expression: "getHeaders()['X-FORWARDED-FOR']"
    

    and the test for this is

       @Test
        fun `SUCCESSFULLY rate by X-FORWARDED-FOR header`() {
        // arrange
       val endpoint = "/api/someendpoint"
    
        // act
        WebTestClient
            .bindToApplicationContext(context)
            .configureClient()
            .defaultHeader("X-FORWARDED-FOR", "1.1.1.1")
            .build()
            .get()
            .uri(endpoint)
            .exchange()
            .expectStatus().isOk
            .expectHeader().valueEquals("X-Rate-Limit-Remaining", "0")
    
        WebTestClient
            .bindToApplicationContext(context)
            .configureClient()
            .defaultHeader("X-FORWARDED-FOR", "1.1.1.2")
            .build()
            .get()
            .uri(endpoint)
            .exchange()
            .expectStatus().isOk
            .expectHeader().valueEquals("X-Rate-Limit-Remaining", "0")
    
        WebTestClient
            .bindToApplicationContext(context)
            .configureClient()
            .defaultHeader("X-FORWARDED-FOR", "1.1.1.1")
            .build()
            .get()
            .uri(endpoint)
            .exchange()
            .expectStatus().isEqualTo(HttpStatus.TOO_MANY_REQUESTS)
            .expectBody().jsonPath("error", "Too many requests!")
    
        // assert
    
    }