From 62d2eb03ac971c982f03dfea3749e3b513e05cf8 Mon Sep 17 00:00:00 2001 From: Maxence Maireaux Date: Thu, 11 Jun 2026 11:08:26 +0200 Subject: [PATCH] fix(api): bound v1 list endpoints with a page size The v1 listInstances and listWorkflows handlers passed a zero-value query, and bunpaginate applies no LIMIT when PageSize == 0 -- so each request loaded the entire workflow_instances / workflows table into memory. Read the page size via bunpaginate.GetPageSize (default 15, max 100, overridable with ?pageSize=), as the v2 handlers already do, while keeping the v1 flat-array response shape. Behavioural note: v1 list responses are now bounded to one page; clients needing more pass ?pageSize=. Adds TestListInstancesIsBounded. --- internal/api/v1/handler_list_instances.go | 10 ++++++ .../api/v1/handler_list_instances_test.go | 35 +++++++++++++++++++ internal/api/v1/handler_list_workflows.go | 12 ++++++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/internal/api/v1/handler_list_instances.go b/internal/api/v1/handler_list_instances.go index 0aa5a68..ca1438c 100644 --- a/internal/api/v1/handler_list_instances.go +++ b/internal/api/v1/handler_list_instances.go @@ -3,6 +3,7 @@ package v1 import ( "net/http" + "github.com/formancehq/go-libs/v3/bun/bunpaginate" "github.com/formancehq/orchestration/internal/workflow" api "github.com/formancehq/orchestration/internal/api" @@ -13,7 +14,16 @@ import ( func listInstances(backend api.Backend) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + // Bound the query: without a page size, bunpaginate applies no LIMIT, + // loading the entire workflow_instances table per request. + pageSize, err := bunpaginate.GetPageSize(r) + if err != nil { + sharedapi.BadRequest(w, "VALIDATION", err) + return + } + runs, err := backend.ListInstances(r.Context(), workflow.ListInstancesQuery{ + PageSize: pageSize, Options: workflow.ListInstancesOptions{ WorkflowID: r.URL.Query().Get("workflowID"), Running: sharedapi.QueryParamBool(r, "running"), diff --git a/internal/api/v1/handler_list_instances_test.go b/internal/api/v1/handler_list_instances_test.go index 4421d12..438e74f 100644 --- a/internal/api/v1/handler_list_instances_test.go +++ b/internal/api/v1/handler_list_instances_test.go @@ -90,3 +90,38 @@ func TestListInstances(t *testing.T) { require.Len(t, instances, 0) }) } + +func TestListInstancesIsBounded(t *testing.T) { + ctx := logging.TestingContext() + + test(t, func(router *chi.Mux, m api.Backend, db *bun.DB) { + w := workflow.New(workflow.Config{}) + _, err := db.NewInsert().Model(&w).Exec(ctx) + require.NoError(t, err) + + for i := 0; i < 20; i++ { + instance := workflow.NewInstance(uuid.NewString(), w.ID) + _, err := db.NewInsert().Model(&instance).Exec(ctx) + require.NoError(t, err) + } + + // Without a page size the default (15) bounds the result instead of + // loading the whole table. + req := httptest.NewRequest(http.MethodGet, "/instances", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Result().StatusCode) + instances := make([]workflow.Instance, 0) + sharedapi.ReadResponse(t, rec, &instances) + require.Len(t, instances, 15) + + // An explicit page size is honoured. + req = httptest.NewRequest(http.MethodGet, "/instances?pageSize=5", nil) + rec = httptest.NewRecorder() + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Result().StatusCode) + instances = make([]workflow.Instance, 0) + sharedapi.ReadResponse(t, rec, &instances) + require.Len(t, instances, 5) + }) +} diff --git a/internal/api/v1/handler_list_workflows.go b/internal/api/v1/handler_list_workflows.go index fd72af0..57757bf 100644 --- a/internal/api/v1/handler_list_workflows.go +++ b/internal/api/v1/handler_list_workflows.go @@ -13,7 +13,17 @@ import ( func listWorkflows(backend api2.Backend) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - workflows, err := backend.ListWorkflows(r.Context(), bunpaginate.OffsetPaginatedQuery[any]{}) + // Bound the query: without a page size, bunpaginate applies no LIMIT, + // loading the entire workflows table per request. + pageSize, err := bunpaginate.GetPageSize(r) + if err != nil { + api.BadRequest(w, "VALIDATION", err) + return + } + + workflows, err := backend.ListWorkflows(r.Context(), bunpaginate.OffsetPaginatedQuery[any]{ + PageSize: pageSize, + }) if err != nil { api.InternalServerError(w, r, err) return