diff --git a/JsonApiToolkit.Tests/Integration/StrictPaginationIntegrationTests.cs b/JsonApiToolkit.Tests/Integration/StrictPaginationIntegrationTests.cs
new file mode 100644
index 0000000..503064d
--- /dev/null
+++ b/JsonApiToolkit.Tests/Integration/StrictPaginationIntegrationTests.cs
@@ -0,0 +1,173 @@
+using System.Net;
+using System.Text.Json;
+using JsonApiToolkit.Extensions;
+using JsonApiToolkit.Models.Documents;
+using JsonApiToolkit.Models.Errors;
+using JsonApiToolkit.Models.Resources;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace JsonApiToolkit.Tests.Integration;
+
+///
+/// Integration tests for runtime strict-pagination behavior (page > totalPages → 404).
+/// Re-uses the QueryTestArticle/QueryTestDbContext fixtures from JsonApiQueryAsyncTests.
+///
+public class StrictPaginationIntegrationTests : IDisposable
+{
+ private readonly IHost _host;
+ private readonly HttpClient _client;
+ private readonly JsonSerializerOptions _jsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ };
+
+ public StrictPaginationIntegrationTests()
+ {
+ var databaseName = $"StrictPaginationTestDb_{Guid.NewGuid()}";
+
+ _host = new HostBuilder()
+ .ConfigureWebHost(webBuilder =>
+ {
+ webBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddDbContext(options =>
+ options.UseInMemoryDatabase(databaseName)
+ );
+ services.AddControllers();
+ services.AddJsonApiToolkit(options =>
+ {
+ options.StrictPagination = true;
+ });
+ })
+ .Configure(app =>
+ {
+ app.UseRouting();
+ app.UseEndpoints(endpoints => endpoints.MapControllers());
+
+ using var scope = app.ApplicationServices.CreateScope();
+ var context =
+ scope.ServiceProvider.GetRequiredService();
+ SeedFiveArticles(context);
+ });
+ })
+ .Build();
+
+ _host.Start();
+ _client = _host.GetTestClient();
+ }
+
+ private static void SeedFiveArticles(QueryTestDbContext context)
+ {
+ for (int i = 1; i <= 5; i++)
+ {
+ context.Articles.Add(
+ new QueryTestArticle
+ {
+ Id = i,
+ Title = $"Article {i}",
+ Content = $"Content {i}",
+ CreatedAt = new DateTime(2024, 1, i),
+ IsPublished = true,
+ ViewCount = i * 10,
+ }
+ );
+ }
+ context.SaveChanges();
+ }
+
+ [Fact]
+ public async Task PageBeyondTotal_Returns404Async()
+ {
+ // 5 articles, page size 2 → 3 total pages. Page 100 must 404.
+ var response = await _client.GetAsync("/api/articles?page[number]=100&page[size]=2");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PageBeyondTotal_ErrorBodyHasMetaAsync()
+ {
+ var response = await _client.GetAsync("/api/articles?page[number]=10&page[size]=2");
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+
+ var content = await response.Content.ReadAsStringAsync();
+ var doc = JsonSerializer.Deserialize(content, _jsonOptions);
+
+ Assert.NotNull(doc?.Errors);
+ var error = Assert.Single(doc.Errors);
+ Assert.Equal("404", error.Status);
+ Assert.Equal(JsonApiErrorCodes.InvalidPageNumber, error.Code);
+ Assert.Equal("page[number]", error.Source?.Parameter);
+ Assert.NotNull(error.Meta);
+ Assert.Equal(10, GetIntFromMeta(error.Meta, "value"));
+ Assert.Equal(3, GetIntFromMeta(error.Meta, "totalPages"));
+ Assert.Equal(5, GetIntFromMeta(error.Meta, "totalResources"));
+ }
+
+ [Fact]
+ public async Task LastPage_Returns200Async()
+ {
+ // Exactly the last page must succeed (boundary check: > not >=).
+ var response = await _client.GetAsync("/api/articles?page[number]=3&page[size]=2&sort=id");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var content = await response.Content.ReadAsStringAsync();
+ var doc = JsonSerializer.Deserialize>(
+ content,
+ _jsonOptions
+ );
+
+ Assert.NotNull(doc?.Data);
+ Assert.Single(doc.Data);
+ Assert.Equal("5", doc.Data.First().Id);
+ }
+
+ [Fact]
+ public async Task EmptyResultWithPaging_DoesNotReturn404Async()
+ {
+ // Filter that matches no rows. With totalCount=0, strict mode must not 404 page=2 —
+ // there are no pages to be wrong about.
+ var response = await _client.GetAsync(
+ "/api/articles?filter[title]=NoSuchArticle&page[number]=2&page[size]=10"
+ );
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var content = await response.Content.ReadAsStringAsync();
+ var doc = JsonSerializer.Deserialize>(
+ content,
+ _jsonOptions
+ );
+
+ Assert.NotNull(doc?.Data);
+ Assert.Empty(doc.Data);
+ }
+
+ private static int GetIntFromMeta(Dictionary meta, string key)
+ {
+ Assert.True(meta.TryGetValue(key, out var raw), $"Missing meta key '{key}'");
+ return raw switch
+ {
+ JsonElement e => e.GetInt32(),
+ int i => i,
+ long l => (int)l,
+ _ => Convert.ToInt32(raw),
+ };
+ }
+
+ public void Dispose()
+ {
+ _client.Dispose();
+ _host.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/JsonApiToolkit/Controllers/JsonApiController.cs b/JsonApiToolkit/Controllers/JsonApiController.cs
index 9ef5f27..76a5671 100644
--- a/JsonApiToolkit/Controllers/JsonApiController.cs
+++ b/JsonApiToolkit/Controllers/JsonApiController.cs
@@ -206,6 +206,26 @@ string resourceType
int totalCount = await filteredQuery.CountAsync().ConfigureAwait(false);
LogCountResults(parameters, totalCount);
+ if (Options.StrictPagination && parameters.Pagination != null && totalCount > 0)
+ {
+ int totalPages = (int)Math.Ceiling(totalCount / (double)parameters.Pagination.Size);
+ if (parameters.Pagination.Number > totalPages)
+ {
+ throw new JsonApiNotFoundException(
+ $"Page {parameters.Pagination.Number} does not exist. "
+ + $"This collection has {totalPages} page(s). Request a page between 1 and {totalPages}.",
+ JsonApiErrorCodes.InvalidPageNumber,
+ new ErrorSource { Parameter = "page[number]" },
+ new Dictionary
+ {
+ ["value"] = parameters.Pagination.Number,
+ ["totalPages"] = totalPages,
+ ["totalResources"] = totalCount,
+ }
+ );
+ }
+ }
+
if (parameters.Pagination != null)
filteredQuery = filteredQuery.ApplyPagination(parameters.Pagination, totalCount);
diff --git a/docs/docs/security.md b/docs/docs/security.md
index 9d2e5f9..69b7cac 100644
--- a/docs/docs/security.md
+++ b/docs/docs/security.md
@@ -183,15 +183,20 @@ builder.Services.AddJsonApiToolkit(options => {
### Behavior
-When `StrictPagination` is enabled, the following values are rejected with 400 Bad Request:
+When `StrictPagination` is enabled, the following values are rejected with 400 Bad Request at parse time:
- `page[number]` less than 1 (e.g., `page[number]=0` or `page[number]=-5`)
- `page[size]` less than 1 (e.g., `page[size]=0` or `page[size]=-10`)
- `page[size]` exceeding `MaxPageSize` (e.g., `page[size]=200` when `MaxPageSize=100`)
+After the count is computed, the following also returns **404 Not Found**:
+
+- `page[number]` greater than the total page count (e.g., `page[number]=100` for a result set with 3 pages). Returns no 404 when the result set is empty (no pages exist).
+
When disabled (default), these values are silently clamped:
- `page[number]` less than 1 becomes 1
+- `page[number]` greater than total pages becomes the last page
- `page[size]` exceeding `MaxPageSize` becomes `MaxPageSize`
### Error Response
diff --git a/docs/docs/upgrade-guide.md b/docs/docs/upgrade-guide.md
index 8ae6b45..5caf543 100644
--- a/docs/docs/upgrade-guide.md
+++ b/docs/docs/upgrade-guide.md
@@ -2,7 +2,38 @@
This document tracks all breaking changes, new features, and migration steps for each version of JsonApiToolkit.
-**Current Version:** 2.0.0
+**Current Version:** 2.1.0
+
+---
+
+## v2.1.0 - Strict Pagination (runtime)
+
+**New Behavior:**
+`StrictPagination` now also rejects requests for a page that does not exist (page number greater than the total number of pages). Previously these requests were clamped to the last page.
+
+When `StrictPagination = true` and the requested `page[number]` exceeds the total page count for the resolved query, the controller returns **404 Not Found** with a JSON:API error document:
+
+```json
+{
+ "errors": [{
+ "status": "404",
+ "code": "INVALID_PAGE_NUMBER",
+ "detail": "Page 10 does not exist. This collection has 3 page(s). Request a page between 1 and 3.",
+ "source": { "parameter": "page[number]" },
+ "meta": {
+ "value": 10,
+ "totalPages": 3,
+ "totalResources": 5
+ }
+ }]
+}
+```
+
+**Edge case:** When a query returns zero resources (for example, a filter that matches nothing), no 404 is raised regardless of the requested page number. There are no pages to be wrong about.
+
+**Default behavior (`StrictPagination = false`) is unchanged**: out-of-range page numbers are clamped to the last page.
+
+**Breaking Changes:** None. Behavior change is opt-in via `StrictPagination`. Parse-time `StrictPagination` errors (invalid page number, invalid page size, page size exceeds maximum) introduced earlier are unchanged.
---