Bug fixes: - Fix nil-pointer panic in sendMessage SOAP fault handler (used err.Error() on nil) - Fix missing return after SOAP fault response (caused fall-through to 200 OK) - Fix timeout=0 shadowing: removed package-level constant, use configured value - Fix receiveMessage not checking SOAP faults from CIIMS Codec hardening: - Replace fragile regex parsing with encoding/xml.Decoder for namespace-agnostic XML - XML-escape all user inputs (user, pass, event) — not just message body - Remove magic-number-based string slicing (split, GetErrMsg) Transport improvements: - Extract Client type with connection pooling (reuse http.Client across requests) - Add CIIMSResponse domain type with IsFault(), ErrorMessage(), Messages() - Add HTTPStatusError and ResponseTooLargeError typed errors - Replace deprecated ioutil.ReadAll with io.ReadAll - Add response body size limit (64 MiB) Security hardening: - Add MaxBytesReader (1MB request body limit) on both endpoints - Add URL allowlist via CIIMS_ALLOWED_SERVERS env var - Add normalizeBaseURL with scheme/host/query validation - Add count bounds validation (1-1000) on /receive - Add server-level timeouts (ReadHeaderTimeout, ReadTimeout, IdleTimeout) Configuration: - Introduce Config struct to replace package-level globals - Add loadConfig() with full validation and error propagation - Add getIntEnv() with positive-value enforcement Test coverage (75 tests, 35 new): - Phase 1: 8 internal tests (nil receiver, error types, SOAP+HTTP500, malformed XML) - Phase 2: 19 pure function tests (normalizeBaseURL, parseAllowedServers, getIntEnv) - Phase 3: 16 handler tests (send/receive success, SOAP fault, network error, HTTP 500, body too large, invalid JSON, missing fields, URL allowlist) - Phase 4: 8 config/router tests (loadConfig, newServer, newRouter) Toolchain: - Upgrade Go 1.13 → 1.22, gin 1.6.3 → 1.10.0, testify 1.5.1 → 1.10.0 Cleanup: - Remove dead code (unused post() function, commented-out defaults) - Replace println with structured logging - Add .gitignore - Rewrite README with API docs, env vars, security considerations
12 KiB
CIIMS Proxy — Refactor & Refine Plan
Based on 2026-07-08-ciimsproxy-codebase-analysis.md Go 1.22 · 4 source files · 16 tests
Objective
Address the remaining correctness gaps, improve abstraction quality, and optimize performance identified in the codebase analysis. The proxy is already functional and stable; this plan targets the next tier of quality improvements without breaking the existing HTTP API contract. SOAP template cleanup must preserve SOAP semantics; any byte-for-byte wire-format change should be explicit and isolated.
Expected outcomes:
receiveMessagedetects and reports CIIMS SOAP faults instead of silently returning empty results.- HTTP non-2xx responses from CIIMS are treated as errors while still allowing SOAP fault bodies to be parsed and returned.
- SOAP response parsing is hardened for expected XML formatting variations; any retained regex patterns are compiled once at startup, not per request.
- A configured HTTP transport client is injected into handlers/tests instead of relying on package-level mutable client state.
- Deprecated
ioutil.ReadAllis replaced withio.ReadAll. - Handler-level (Gin endpoint) tests are added.
- Domain types and a
Configstruct improve type safety and testability. - Template indentation is normalized for readability only if the team accepts the resulting byte-for-byte SOAP request change.
Implementation Plan
Phase 1 — Correctness (High Priority)
1. Add SOAP Fault Detection to receiveMessage
- In
cmd/main/main.go, after the outbound CIIMS call inreceiveMessage, callinternal.GetErrMsg(resp)and return500with the fault text if non-empty. - If the outbound call returns a typed HTTP status error with a response body, inspect that body with
internal.GetErrMsgbefore falling back to the transport/status error message. - Log the SOAP fault at error level, consistent with
sendMessage. - Add a handler-level test in
cmd/main/main_test.go: mock CIIMS returnsErrMsgfor a receive call, verify/receivereturns500 {"error":"<fault text>"}instead of200 {"msgs":[]}.
2. Check HTTP Status Codes from CIIMS
- In
internal/http.go, afterclient.Do(req), checkresp.StatusCode. - Always read and close the response body once. If status is not 2xx, return the body string alongside a typed error such as
HTTPStatusError{StatusCode, Status, Body}. - Make
HTTPStatusError.Error()include the status code and a truncated/sanitized body snippet, while still exposing the full body field for SOAP fault parsing by the handler. - Update
sendMessageandreceiveMessageerror handling: whenerr != nil, first parserespor the typed error body for a SOAP fault; if found, return that fault, otherwise return the transport/status error. - Add a test: mock CIIMS returns
500 Internal Server Error, verifySend()returns an error. - Add a test: mock CIIMS returns HTTP
500with SOAP fault XML, verify the handler returns the SOAP fault text rather than a generic HTTP status error. - Add a test: mock CIIMS returns
200 OK, verifySend()succeeds normally.
Phase 2 — Performance (High Priority)
3. Harden Response Parsing and Pre-compile Patterns
- Prefer replacing regex-based parsing in
GetMsgsandGetErrMsgwithencoding/xml.Decodertoken parsing that matches elements by local name (string,errorMessage) regardless of namespace prefix. - If regex is retained as a short-term step, update patterns to handle expected formatting variations such as attributes on
<prefix:string>and multiline content. - If regex is retained, move
regexp.MustCompile(msgExp)andregexp.MustCompile(errExp)from function bodies to package-levelvardeclarations. - Name them
msgRegexanderrRegex. - Update
GetMsgsandGetErrMsgto use the pre-compiled variables. - Add tests for multiline string payloads, attributes on string elements, whitespace variations, and different namespace prefixes.
- Verify all existing tests pass without changes unless parser behavior is intentionally broadened.
4. Inject a Configured HTTP Client
- Do not introduce package-level mutable client state such as
var httpClient *http.ClientplusInitClient; this risks nil-client bugs and test pollution. - In
internal/http.go, introduce a transport type such astype Client struct { httpClient *http.Client }. - Add
NewClient(timeoutSec int) (*Client, error)to validate/default the timeout and construct the underlyinghttp.Client. - Move outbound send behavior to a method such as
func (c *Client) Send(url, msg string) (string, error). - Construct the client in
main()after resolving configuration and pass it to handlers through aConfig/handler struct or closure. - Update tests to create their own
internal.Clientinstances so timeout settings do not leak between tests. - Add a test that verifies the injected client timeout is honored.
- Ensure the existing
TestSend_NetworkTimeouttest still passes.
Phase 3 — Modernization (Medium Priority)
5. Replace Deprecated ioutil.ReadAll
- In
internal/http.go, replaceioutil.ReadAll(resp.Body)withio.ReadAll(resp.Body). - Remove the
"io/ioutil"import and add"io". - Verify build and tests pass.
6. Add Handler-Level Tests
- Create
cmd/main/main_test.gowith Gin'shttptestsetup. - Test
POST /sendwith valid JSON → mock CIIMS returns success → assert200 {"error":""}. - Test
POST /sendwith valid JSON → mock CIIMS returns SOAP fault → assert500with fault text. - Test
POST /sendwith missing required field → assert400. - Update handlers to detect
*http.MaxBytesErrorfrom binding and returnhttp.StatusRequestEntityTooLarge. - Test
POST /sendwith body exceeding 1 MB → assert413. - Test
POST /receivewith valid JSON → mock CIIMS returns messages → assert200 {"msgs":[...]}. - Test
POST /receivewithcount= 0 → assert400. - Test
POST /receivewithcount= 1001 → assert400. - Test
POST /receivewith valid JSON → mock CIIMS returns SOAP fault → assert500. - Test
GET /ping→ assert200 {"message":"pong"}.
Phase 4 — Abstraction (Low Priority)
7. Introduce Domain Types
- In
internal/codec.go, define aCIIMSResponsestruct with aRawXML stringfield. - Add methods:
IsFault() bool,ErrorMessage() string,Messages() []string. - Update the injected client's
Send()method to return(*CIIMSResponse, error)instead of(string, error). - Preserve access to any non-2xx response body through
CIIMSResponse.RawXMLand/or the typed status error so SOAP faults in HTTP 500 responses remain parseable. - Update
sendMessageandreceiveMessageto use the new methods. - Update all tests to use the new return type.
- Ensure no change in external behavior.
8. Introduce a Config Struct
- In
cmd/main/main.go, define aConfigstruct with fieldsServerURL,Timeout,Listen. - Include the injected CIIMS client/transport in
Configor in a handler struct. - Parse environment variables into a
Configvalue inmain(). - Pass
Configto handler functions via closure (or a handler struct). - Remove package-level
defaultURLandtimeoutvariables. - Update
getURLto accept the server URL as a parameter. - Verify all handler tests pass with the new structure.
9. Remove Redundant Send Wrapper
- In
internal/http.go, deletepostSimafter moving its logic into the injected client method. - Decide whether to keep a package-level
Send(url, message, timeout)compatibility wrapper for legacy internal tests; if kept temporarily, mark it as a thin compatibility helper and keep production handlers on the injected client. - Otherwise, update all callers (in
main.goand tests) to useclient.Send(...). - Verify build and all tests pass.
Phase 5 — Cleanup (Low Priority)
10. Normalize Template Indentation
- Confirm this is acceptable as a byte-for-byte SOAP request change before implementing.
- In
internal/codec.go, makesendtplandreceivetpluse consistent indentation (all spaces or all tabs). - Update the golden-file test constants in
internal/codec_test.go(SendEsp,ReceiveEsp) to match. - Verify
TestSendandTestReceivepass with the updated expected strings.
11. Remove Unused gommon/log Dependency
- In
cmd/main/main.go, replacegithub.com/labstack/gommon/logwith Go's standardlogpackage. - Replace
log.Infof→log.Printf,log.Errorf→log.Printf("[ERROR] ...")or uselogwith prefixes. - Remove
github.com/labstack/gommonfromgo.modand rungo mod tidy. - Verify build and all tests pass.
Verification Criteria
POST /receivewith a CIIMS SOAP fault returns500 {"error":"<fault text>"}, not200 {"msgs":[]}.- CIIMS returning HTTP 500 produces an error from
Send(), not a successful response. - CIIMS returning HTTP 500 with a SOAP fault body returns the SOAP fault text to the caller.
- Oversized request bodies return
413, not a generic400. go test ./...shows all tests passing; response parsing handles multiline/attribute/namespace-prefix variations.- HTTP client timeout is configured through an injected client/transport and does not rely on package-level mutable initialization.
ioutil.ReadAllno longer appears in the codebase.cmd/main/main_test.goexists with at least 9 handler-level tests covering all error paths.internal.CIIMSResponsetype exists withIsFault(),ErrorMessage(),Messages()methods.cmd/mainhas no package-leveldefaultURLortimeoutvariables.internal/http.gohas nopostSim; production code uses the injected clientSendmethod.sendtplandreceivetpluse consistent indentation only if the wire-format change was explicitly accepted.github.com/labstack/gommonis absent fromgo.mod.go vet ./...andgo build ./...produce no errors.
Execution Order
Phase 1 (Correctness) ──► Phase 2 (Performance) ──► Phase 3 (Modernization)
│
Phase 5 (Cleanup) ◄──────── Phase 4 (Abstraction) ◄────────┘
Phases 1 and 2 should be completed first as they fix remaining bugs and transport/testability issues. Phase 4's Config/injected-client work may be pulled earlier if it simplifies Phase 2. Phases 3–5 are quality-of-life improvements that can be done in any order, but Phase 4 should precede Phase 5 since removing gommon/log may be affected by the Config struct refactor.
Potential Risks and Mitigations
-
HTTP status code checking breaks CIIMS compatibility
- Some SOAP services return
500with a SOAP fault in the body (which is valid SOAP). The current code already handles this viaGetErrMsg. The fix should check for 2xx and treat everything else as a transport error, but still allow the SOAP fault body to be extracted if present. - Mitigation: On non-2xx, return a typed
HTTPStatusErrorthat exposes the response body and also return/preserve that body in the response value. The handler must parse SOAP faults first, then fall back to the status error.
- Some SOAP services return
-
Injected
http.Clientchanges timeout behavior- The current code creates a new client per request with the configured timeout. An injected client has a fixed timeout set at construction.
- Mitigation: This is the desired production behavior, and tests should construct isolated clients. If per-request timeout overrides are needed later, use
context.WithTimeouton the request context.
-
CIIMSResponseabstraction changes the return type- All callers of
Send()must be updated. - Mitigation: This is a mechanical refactor. The compiler will catch all call sites. Update them in one pass.
- All callers of
-
Removing
gommon/logchanges log output format- Standard
logpackage has a different default format (timestamps with date/time). - Mitigation: This is acceptable—Gin already uses its own logger. Standardizing on
logreduces dependencies and is more idiomatic.
- Standard
-
Template indentation change breaks golden-file tests
TestSendandTestReceivedo exact string comparison.- Mitigation: Update the expected constants (
SendEsp,ReceiveEsp) in the same commit. The tests themselves verify correctness.