From c72139919f341c5e751c7cae7d770bf752827147 Mon Sep 17 00:00:00 2001 From: Bruno do Nascimento Maciel Date: Fri, 1 May 2026 18:58:46 -0300 Subject: [PATCH 1/3] Remove connector-level CORS configuration (breaking change) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the :allowed-origins configuration option and the with-default-interceptors call that configured CORS at the service component level. CORS and other default request interceptors must now be configured per-route in the consuming application. Changes: - Remove allow-all-origins var and allowed-origins function - Remove with-default-interceptors call from service initialization - Update README with new Interceptors section explaining options - Remove CORS integration tests that test removed functionality - Version bump: 0.3.8 → 0.4.0 (breaking change) Co-Authored-By: Claude Haiku 4.5 --- CHANGELOG.md | 6 ++++ README.md | 28 +++++++++++++-- project.clj | 2 +- src/service/component.clj | 15 ++------ test/integration/cors_test.clj | 65 ---------------------------------- 5 files changed, 35 insertions(+), 81 deletions(-) delete mode 100644 test/integration/cors_test.clj diff --git a/CHANGELOG.md b/CHANGELOG.md index 87acc63..e5e4944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). +## 0.4.0 - 2026-05-01 + +### Changed + +- **Breaking:** Removed connector-level CORS configuration (`:allowed-origins` option is no longer supported). CORS and other default interceptors must now be configured per-route in the consuming application using `io.pedestal.connector/with-default-interceptors` or Pedestal route-level interceptors. + ## 0.3.8 - 2026-05-01 ### Added diff --git a/README.md b/README.md index 8eab321..0bfb6b6 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ The service component accepts configuration through the `:service` key in your c | `:host` | String | Yes | — | The host address to bind the server to (e.g., `"0.0.0.0"`). | | `:port` | Integer | Yes | — | The port number to listen on (e.g., `8080`). | | `:idle-timeout-ms` | Integer | No | `30000` | Jetty idle timeout in milliseconds. Connections idle beyond this duration are closed. | -| `:allowed-origins` | Collection\ | No | Allow all origins | A collection of allowed origin strings for CORS. When omitted or empty, all origins are allowed. | | `:min-threads` | Integer | No | `8` | Minimum number of threads kept alive in the Jetty thread pool. | | `:max-threads` | Integer | No | `50` | Maximum number of concurrent threads. Acts as a concurrency cap for both platform and virtual thread modes. | | `:max-queue-size` | Integer | No | `200` | Maximum number of requests that can queue while all threads are busy (platform threads only). Requests beyond this limit are rejected with HTTP 503. | @@ -48,7 +47,6 @@ Uses Jetty's `QueuedThreadPool` backed by a `BlockingArrayQueue` of size `:max-q {:service {:host "0.0.0.0" :port 8080 :idle-timeout-ms 60000 - :allowed-origins ["https://example.com" "https://app.example.com"] :min-threads 8 :max-threads 200 :max-queue-size 500 @@ -66,7 +64,31 @@ Uses Jetty's `QueuedThreadPool` backed by a `BlockingArrayQueue` of size `:max-q > **Note:** If `:idle-timeout-ms` is not provided, a default of **30 seconds** (`30000` ms) is applied to prevent stalled connections from tying up server resources. -> **Note:** If `:allowed-origins` is not provided or is empty, the server will accept requests from **any origin**. In production, explicitly list trusted origins to prevent unwanted cross-origin access. +## Interceptors + +CORS and other default request interceptors must be configured **in your consuming application**, not at the component level. + +The service component provides two built-in interceptors: +- `error-handler-interceptor` — handles exception-to-response conversion. +- `components-interceptor` — injects the Integrant components map into the request context. + +To add CORS, authentication, rate limiting, or other cross-cutting concerns, use one of these approaches in your application: + +**Option 1: Per-route interceptors** + +Define interceptors on individual routes in your route definitions: +```clojure +["/api/resource" + {:get {:handler my-handler + :interceptors [cors-interceptor auth-interceptor]}}] +``` + +**Option 2: Connector-level default interceptors** + +Use `io.pedestal.connector/with-default-interceptors` in your route setup to apply interceptors to all routes: +```clojure +(io.pedestal.connector/with-default-interceptors connector :allowed-origins cors-origins) +``` ## License diff --git a/project.clj b/project.clj index add33e6..ef6b6eb 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject net.clojars.macielti/service "0.3.8" +(defproject net.clojars.macielti/service "0.4.0" :description "Service is a Pedestal service Integrant component" diff --git a/src/service/component.clj b/src/service/component.clj index 97a5668..c3a99f3 100644 --- a/src/service/component.clj +++ b/src/service/component.clj @@ -14,8 +14,6 @@ (def ^:private default-max-threads 50) (def ^:private default-max-queue-size 200) -(def ^:private allow-all-origins (constantly true)) - (defn- java-version [] (let [version (System/getProperty "java.version") major-version (Integer/parseInt (first (str/split version #"\.")))] @@ -37,15 +35,10 @@ 60000 (BlockingArrayQueue. max-queue-size))))) -(defn ^:private allowed-origins - "Returns the allowed-origins value for Pedestal. - When a collection of origin strings is provided, returns it as a set. - Otherwise, allows all origins." - [origins] - (if (not-empty origins) - (set origins) - allow-all-origins)) +; CORS and other default request interceptors must be configured per-route in the consuming application. +; Use `io.pedestal.connector/with-default-interceptors` or Pedestal route-level interceptors +; to add CORS, authentication, or other cross-cutting concerns to your routes. (defmethod ig/init-key ::service [_ {:keys [components]}] (log/info :starting ::service) @@ -57,14 +50,12 @@ use-virtual-threads (if (contains? service-config :use-virtual-threads) (:use-virtual-threads service-config) true) - allowed-origins' (:allowed-origins service-config) connector (-> {:host (:host service-config) :port (:port service-config) :type :jetty :router :sawtooth :initial-context {} :join? false} - (io.pedestal.connector/with-default-interceptors :allowed-origins (allowed-origins allowed-origins')) (io.pedestal.connector/with-interceptors [io.interceptors/error-handler-interceptor (io.interceptors/components-interceptor components)]) (io.pedestal.connector/with-routes (:routes components)) diff --git a/test/integration/cors_test.clj b/test/integration/cors_test.clj deleted file mode 100644 index 372264c..0000000 --- a/test/integration/cors_test.clj +++ /dev/null @@ -1,65 +0,0 @@ -(ns cors-test - (:require [clj-http.client :as http] - [clojure.test :refer [is testing]] - [common-clj.integrant-components.config :as component.config] - [common-clj.integrant-components.prometheus :as component.prometheus] - [common-clj.integrant-components.routes :as component.routes] - [integrant.core :as ig] - [io.pedestal.service.interceptors :as pedestal.service.interceptors] - [matcher-combinators.test :refer [match?]] - [schema.test :as s] - [service.component :as component.service] - [service.interceptors :as interceptors])) - -(def routes [["/test" :get [interceptors/http-request-in-handle-timing-interceptor - pedestal.service.interceptors/json-body - (fn [{{:keys [config]} :components}] - {:status 200 - :body config})] - :route-name :test]]) - -(def system-components - {::component.config/config {:path "test/resources/config.example.edn" - :env :test} - ::component.routes/routes {:routes routes} - ::component.prometheus/prometheus {:metrics []} - ::component.service/service {:components {:config (ig/ref ::component.config/config) - :prometheus (ig/ref ::component.prometheus/prometheus) - :routes (ig/ref ::component.routes/routes)}}}) - -(s/deftest allowed-origins-allow-all-test - (testing "When no allowed-origins is configured, all origins are permitted" - (let [system (ig/init system-components)] - - (is (match? {:status 200 - :headers {"Access-Control-Allow-Origin" "https://any-origin.com"}} - (http/get "http://localhost:8080/test" - {:headers {"Origin" "https://any-origin.com" - "authorization" "Bearer test-token"} - :throw-exceptions false}))) - - (ig/halt! system)))) - -(s/deftest allowed-origins-restricted-test - (testing "When allowed-origins is configured, only specified origins are permitted" - (let [system (ig/init (assoc-in system-components - [::component.config/config :overrides] - {:service {:host "0.0.0.0" - :port 8080 - :allowed-origins ["https://allowed.com" "https://trusted.com"]}}))] - - (is (match? {:status 200 - :headers {"Access-Control-Allow-Origin" "https://allowed.com"}} - (http/get "http://localhost:8080/test" - {:headers {"Origin" "https://allowed.com" - "authorization" "Bearer test-token"} - :throw-exceptions false}))) - - (is (match? {:status 403 - :headers (fn [headers] (nil? (get headers "Access-Control-Allow-Origin")))} - (http/get "http://localhost:8080/test" - {:headers {"Origin" "https://not-allowed.com" - "authorization" "Bearer test-token"} - :throw-exceptions false}))) - - (ig/halt! system)))) From f10ed73810af5699d3a056b7eef6ab570b31f85b Mon Sep 17 00:00:00 2001 From: Bruno do Nascimento Maciel Date: Fri, 1 May 2026 19:04:01 -0300 Subject: [PATCH 2/3] lint fix --- src/service/component.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/service/component.clj b/src/service/component.clj index c3a99f3..13eb68e 100644 --- a/src/service/component.clj +++ b/src/service/component.clj @@ -35,7 +35,6 @@ 60000 (BlockingArrayQueue. max-queue-size))))) - ; CORS and other default request interceptors must be configured per-route in the consuming application. ; Use `io.pedestal.connector/with-default-interceptors` or Pedestal route-level interceptors ; to add CORS, authentication, or other cross-cutting concerns to your routes. From b7d07301f69c8109608a5313daf656bad63e294d Mon Sep 17 00:00:00 2001 From: Bruno do Nascimento Maciel Date: Fri, 1 May 2026 19:57:04 -0300 Subject: [PATCH 3/3] commit to remote --- test/integration/component_test.clj | 26 ++++++++++++++----- test/integration/idle_timeout_test.clj | 29 ++++++++++++++++----- test/integration/thread_pool_test.clj | 36 ++++++++++++++++++-------- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/test/integration/component_test.clj b/test/integration/component_test.clj index c6da93b..d251cff 100644 --- a/test/integration/component_test.clj +++ b/test/integration/component_test.clj @@ -8,7 +8,7 @@ [iapetos.export :as export] [integrant.core :as ig] [io.pedestal.connector.test :as test] - [io.pedestal.service.interceptors :as pedestal.service.interceptors] + [io.pedestal.interceptor :as pedestal.interceptor] [matcher-combinators.test :refer [match?]] [schema.core :as schema] [schema.test :as s] @@ -17,19 +17,33 @@ (def test-state (atom nil)) +(def parse-json-body-interceptor + (pedestal.interceptor/interceptor + {:name ::parse-json-body + :enter (fn [ctx] + (let [body (-> ctx :request :body) + body-str (if body + (slurp body) + "")] + (assoc-in ctx [:request :json-params] + (if (str/blank? body-str) + "" + (json/decode body-str true)))))})) + (def routes [["/test" :get [interceptors/http-request-in-handle-timing-interceptor - pedestal.service.interceptors/json-body (fn [{{:keys [config]} :components}] {:status 200 - :body config})] + :headers {"Content-Type" "application/json;charset=UTF-8"} + :body (json/encode config)})] :route-name :test] - ["/schema-validation-interceptor-test" :post [(interceptors/wire-in-body-schema {:test schema/Str + ["/schema-validation-interceptor-test" :post [parse-json-body-interceptor + (interceptors/wire-in-body-schema {:test schema/Str (schema/optional-key :type) schema/Keyword}) - pedestal.service.interceptors/json-body (fn [{:keys [json-params]}] (reset! test-state json-params) {:status 200 - :body {:test :schema-ok}})] + :headers {"Content-Type" "application/json;charset=UTF-8"} + :body (json/encode {:test :schema-ok})})] :route-name :schema-validation-interceptor-test]]) (def system-components diff --git a/test/integration/idle_timeout_test.clj b/test/integration/idle_timeout_test.clj index 78a4ee1..29f4b6f 100644 --- a/test/integration/idle_timeout_test.clj +++ b/test/integration/idle_timeout_test.clj @@ -1,11 +1,13 @@ (ns idle-timeout-test - (:require [clj-http.client :as http] + (:require [cheshire.core :as json] + [clj-http.client :as http] + [clojure.string :as str] [clojure.test :refer [is testing]] [common-clj.integrant-components.config :as component.config] [common-clj.integrant-components.prometheus :as component.prometheus] [common-clj.integrant-components.routes :as component.routes] [integrant.core :as ig] - [io.pedestal.service.interceptors :as pedestal.service.interceptors] + [io.pedestal.interceptor :as pedestal.interceptor] [matcher-combinators.test :refer [match?]] [schema.test :as s] [service.component :as component.service] @@ -13,17 +15,30 @@ (:import (java.net Socket SocketTimeoutException) (java.io BufferedReader InputStreamReader PrintWriter))) +(def parse-json-body-interceptor + (pedestal.interceptor/interceptor + {:name ::parse-json-body + :enter (fn [ctx] + (let [body (-> ctx :request :body) + body-str (if body + (slurp body) + "")] + (assoc-in ctx [:request :json-params] + (if (str/blank? body-str) + "" + (json/decode body-str true)))))})) + (def routes [["/test" :get [interceptors/http-request-in-handle-timing-interceptor - pedestal.service.interceptors/json-body (fn [{{:keys [config]} :components}] {:status 200 - :body config})] + :headers {"Content-Type" "application/json;charset=UTF-8"} + :body (json/encode config)})] :route-name :test] - ["/slow" :get [pedestal.service.interceptors/json-body - (fn [_] + ["/slow" :get [(fn [_] (Thread/sleep 2000) {:status 200 - :body {:message "slow response"}})] + :headers {"Content-Type" "application/json;charset=UTF-8"} + :body (json/encode {:message "slow response"})})] :route-name :slow]]) (def system-components diff --git a/test/integration/thread_pool_test.clj b/test/integration/thread_pool_test.clj index 034632b..e411b26 100644 --- a/test/integration/thread_pool_test.clj +++ b/test/integration/thread_pool_test.clj @@ -1,34 +1,48 @@ (ns thread-pool-test - (:require [clj-http.client :as http] + (:require [cheshire.core :as json] + [clj-http.client :as http] [clojure.string :as str] [clojure.test :refer [is testing]] [common-clj.integrant-components.config :as component.config] [common-clj.integrant-components.prometheus :as component.prometheus] [common-clj.integrant-components.routes :as component.routes] [integrant.core :as ig] - [io.pedestal.service.interceptors :as pedestal.service.interceptors] + [io.pedestal.interceptor :as pedestal.interceptor] [matcher-combinators.test :refer [match?]] [schema.test :as s] [service.component :as component.service] [service.interceptors :as interceptors])) +(def parse-json-body-interceptor + (pedestal.interceptor/interceptor + {:name ::parse-json-body + :enter (fn [ctx] + (let [body (-> ctx :request :body) + body-str (if body + (slurp body) + "")] + (assoc-in ctx [:request :json-params] + (if (str/blank? body-str) + "" + (json/decode body-str true)))))})) + (def routes [["/test" :get [interceptors/http-request-in-handle-timing-interceptor - pedestal.service.interceptors/json-body (fn [{{:keys [config]} :components}] {:status 200 - :body config})] + :headers {"Content-Type" "application/json;charset=UTF-8"} + :body (json/encode config)})] :route-name :test] - ["/blocking" :get [pedestal.service.interceptors/json-body - (fn [_] + ["/blocking" :get [(fn [_] (Thread/sleep 500) {:status 200 - :body {:message "done"}})] + :headers {"Content-Type" "application/json;charset=UTF-8"} + :body (json/encode {:message "done"})})] :route-name :blocking] - ["/thread-info" :get [pedestal.service.interceptors/json-body - (fn [_] + ["/thread-info" :get [(fn [_] {:status 200 - :body {:virtual? (-> (Thread/currentThread) .getClass .getName - (str/includes? "VirtualThread"))}})] + :headers {"Content-Type" "application/json;charset=UTF-8"} + :body (json/encode {:virtual? (-> (Thread/currentThread) .getClass .getName + (str/includes? "VirtualThread"))})})] :route-name :thread-info]]) (def system-components