ADR: natywny transport SOAP dla GUS BIR 1.2

Decyzja przyjęta 2026-08-23

Kontekst

Wersja 1.x delegowała komunikację z GUS do gusapi/gusapi. Utrudniało to pełną kontrolę nad cyklem życia klucza i SID, klasyfikacją pustych odpowiedzi, dekodowaniem MTOM oraz stabilnością publicznego API pakietu.

Produkcyjny klucz, identyfikator sesji i surowe XML nie mogą trafić do komunikatu wyjątku, jego poprzedniej przyczyny, stosu, debug output ani serializowanego payloadu aplikacji.

Decyzja

Pakiet używa własnego, ograniczonego transportu HTTP opartego na libcurl. Koperty SOAP 1.2 i nagłówki WS-Addressing buduje SoapEnvelopeBuilder, CurlBirHttpSender wykonuje pojedyncze żądanie do endpointu wybranego przez zamknięty enum, a SoapResponseDecoder klasyfikuje zwykłe XML i multipart/related. Wewnętrzny XML raportów dekoduje osobna warstwa.

Sender utrzymuje jeden uchwyt cURL w obrębie scope klienta i resetuje go przed oraz po każdym żądaniu. Dzięki temu libcurl może ponownie użyć połączenia, cache DNS i sesji TLS, natomiast body, callbacki, nagłówki i sid nie przechodzą do następnej operacji.

Konfiguracja transportu jest walidowana bez niejawnego lub stratnego rzutowania i bez clampowania. Dopuszcza 1..60 sekund na połączenie, 1..300 sekund na całe żądanie oraz 1..50000000 bajtów odpowiedzi. Wartość spoza zakresu kończy konstrukcję przed otwarciem połączenia. Sender wymaga TLS 1.2 lub nowszego zarówno dla endpointu GUS, jak i dla jawnego albo wykrytego przez libcurl proxy HTTPS.

CurlBirHttpSender przekazuje nagłówki, w tym opcjonalny sid, oraz kopertę XML do krótkiej granicy NativeCurlExecutor::execute(). Oba parametry są oznaczone #[SensitiveParameter]; executor tylko ustawia wrażliwe opcje i wykonuje cURL, a sender resetuje uchwyt w bloku finally. Granica I/O jest zatem jawna i osłonięta, ale nie udaje, że XML ani SID nigdy nie są jej argumentami.

Oficjalne WSDL i XSD służą do weryfikacji kontraktu oraz fixtures, ale nie są kopiowane do dystrybucji. Eliminuje to zdalne pobieranie WSDL w runtime, cache WSDL i niejasność dotyczącą redystrybucji plików GUS.

Kontrakt domenowy

Limiter żądań

Klient rozwiązywany przez Laravel używa domyślnie CacheBirRequestLimiter. Stan jest współdzielony przez cache obsługujący atomowe blokady, rozdzielony według środowiska i skrótu klucza API. Konserwatywny lokalny model waży koszt operacji, modeluje budżet sekundowy przez czas teoretycznego nadejścia podobny do GCRA, a budżety minutowy i godzinowy prowadzi w stałych oknach kalendarzowych według czasu Europe/Warsaw. Początek okna jest zapisywany jako epoka, dzięki czemu dwie powtórzone godziny podczas zmiany czasu nie współdzielą licznika. Jest to lokalny mechanizm dopuszczenia wyprowadzony z opublikowanych limitów, a nie deklaracja dokładnego, nieopublikowanego algorytmu serwera GUS. Każda operacja SOAP kosztuje jedną jednostkę, zaś wyszukiwanie batch kosztuje tyle jednostek, ile zawiera identyfikatorów.

Poza wewnętrznym scope pojedyncze acquire() może wykonać pacing długu sekundowego łącznie przez najwyżej 1 sekundę, zawsze po zwolnieniu locka. Gateway otwiera i zamyka wielowywołaniowy scope przez jawne beginRateLimitScope() i endRateLimitScope() w try/finally, bez bound closure przechwytującego klienta lub sesję. Po pierwszej udanej rezerwacji w tym scope każde następne, wewnętrzne acquire() może czekać łącznie do 7 sekund. Diagnostyka i recovery obejmują kilka takich rezerwacji, więc całe wywołanie może trwać dłużej niż 7 sekund. Okna minutowe i godzinowe nie są usypiane; ich wyczerpanie albo przekroczenie dopuszczalnego pacingu zgłasza BirRateLimitException przed siecią.

Limiter akceptuje wyłącznie obiekt o dokładnej klasie Illuminate\Cache\Repository i store z allowlisty: ArrayStore, DatabaseStore, FileStore, MemcachedStore albo RedisStore, który zarazem implementuje LockProvider. Dekoratory i inne implementacje, między innymi TaggedCache, DynamoDbStore, FailoverStore, MemoizedStore oraz NullStore, kończą operację fail closed. Lease locka trwa 30 sekund, oczekiwanie przy contention najwyżej 1 sekundę, a własność jest ponownie sprawdzana przed zapisem stanu i po nim.

W aplikacji wielohostowej zalecany jest jeden współdzielony Redis. Store lokalny, taki jak pamięć albo pliki, nie koordynuje niezależnych hostów. Fail closed dla dekoratorów i store spoza allowlisty zapobiega przypadkowemu rozdzieleniu globalnego budżetu między backendy.

Ręcznie konstruowany NativeSoapTransport wymaga jawnej implementacji BirRequestLimiterInterface; nie istnieje niejawny fallback bez limitu. UnlimitedBirRequestLimiter pozostaje świadomym opt-outem wyłącznie dla testu bez sieci albo integracji, w której inna warstwa koordynuje wszystkich użytkowników danego klucza.

Odrzucone warianty

Wariant Powód odrzucenia
Dalsze użycie gusapi/gusapi Brak pełnej kontroli nad sekretami, sesją, wyjątkami i publicznymi typami pakietu.
Zdalne WSDL w runtime Dodatkowy punkt awarii, zależność od sieci przed operacją oraz trudniejsze timeouty i cache.
Dołączone WSDL/XSD Nie jest potrzebne do wykonania sześciu znanych operacji, a wymagałoby odrębnej decyzji o pochodzeniu i redystrybucji.
SoapClient::__doRequest() Przechowuje pełne body poza kontrolą limitu strumieniowego, nie daje twardego deadline'u całej wymiany i może udostępnić argument XML globalnemu handlerowi błędów przed przechwyceniem wyjątku.

Granice bezpieczeństwa

Dowody i weryfikacja

Konsekwencje