From 4f8762e5b88e53266b5f43001f60de632c3239d9 Mon Sep 17 00:00:00 2001 From: Ilya Brin Date: Mon, 23 Mar 2026 00:46:23 -0500 Subject: [PATCH] Fix thread-safety bug in OpenAPIDereferencer31 OpenAPIDereferencer31 is a singleton (via DereferencersFactory) but stored per-call state in mutable instance fields `openAPI` and `result`. When multiple threads parse different OpenAPI 3.1 specs concurrently, these fields get overwritten, causing one parse to return another's result. Fix: make `openAPI` and `result` local variables in `dereference()` instead of instance fields, since they are only used within that method. Also remove the unused `messages` instance field which had the same problem. Fixes https://github.com/swagger-api/swagger-parser/issues/2293 Co-Authored-By: Claude Opus 4.6 --- .../reference/OpenAPIDereferencer31.java | 10 +-- ...OpenAPIDereferencer31ThreadSafetyTest.java | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIDereferencer31ThreadSafetyTest.java diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java index 7c349f5d3c..9ac62a9873 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/OpenAPIDereferencer31.java @@ -11,15 +11,9 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; -import java.util.Set; public class OpenAPIDereferencer31 implements OpenAPIDereferencer { - protected final Set messages = new HashSet<>(); - - private OpenAPI openAPI; - private SwaggerParseResult result; - public boolean canDereference(DereferencerContext context) { if (context.openApi != null && context.openApi.getOpenapi().startsWith("3.1")) { return true; @@ -38,8 +32,8 @@ public void dereference(DereferencerContext context, Iterator> futures = new ArrayList<>(); + for (int i = 0; i < iterations; i++) { + final String spec = (i % 2 == 0) ? specA : specB; + final String expectedTitle = (i % 2 == 0) ? "SpecA" : "SpecB"; + futures.add(pool.submit(() -> { + String actual = parseAndGetTitle(spec); + assertEquals("Parsed title should match input spec (concurrent race detected)", expectedTitle, actual); + return actual; + })); + } + + for (Future future : futures) { + future.get(); + } + } finally { + pool.shutdown(); + } + } +}