2026-08-19 - v1.26
    + fix: kvp2json's and kvp2str's events->{keys} callback had no
      deduplication of its return value, unlike new_request()'s
      add_headers_keys path (HAC-022). A duplicate key caused a CODE
      value at that key to be invoked once per occurrence instead of
      once - side effects fired multiple times, and kvp2str produced a
      genuinely malformed doubled-key output ("a=computed-1&a=computed-2")
      rather than an intentional multi-value array. Deduplicated
      (first occurrence wins, order otherwise preserved, not sorted) in
      both encoders (HAC-129, t/94)
    + fix: same bug class as HAC-129, in new_request()'s headers_keys
      callback (a full override of which header keys to consider,
      similar to kvp2json's/kvp2str's events->{keys}) - a duplicate key
      silently double-fired before_header/after_header for that key.
      Deduplicated the same order-preserving way (HAC-130, t/95)
    + test: coverage added for three untested condition-coverage rows on
      already-correct behavior: _build_retry's non-numeric-count
      passthrough (HAC-131, t/96), new_request's skip-header guard for a
      headers_keys-listed key never present in %headers at all (HAC-132,
      t/97), and prepare_request's Basic-auth guard for an empty username
      with a non-empty password (HAC-133, t/98). Client.pm's condition
      coverage reaches 100% as a result

2026-08-19 - v1.25
    + fix: send()'s DEBUG_IN_OUT/DEBUG_SEND_OUT "-- REQUEST --" dump printed
      $response->request->as_string instead of the $req object it actually
      built and passed to $ua->request($req). LWP::UserAgent follows
      redirects automatically for GET/HEAD by default (max_redirect=7,
      requests_redirectable=[GET,HEAD]); after a redirect $response->request
      is the LAST request in the chain - a different object, pointing at the
      redirected-to URL, not the request this call actually issued. So the
      debug dump could silently show the wrong request (wrong URL, and for
      a 303-redirected POST, even the wrong method) whenever a GET/HEAD hit
      a redirect. Fixed by hoisting $req to be captured once built and using
      it directly for the REQUEST dump; the RESPONSE dump is unaffected, it
      correctly reflects the final response as received (HAC-121, t/87)
    + fix: _build_retry() clamped a negative retry count to 0 but had no
      equivalent clamp for a negative delay, so the public "retry"
      attribute (kept specifically for external callers who read
      $api->retry directly) exposed an unclamped negative delay even
      though send() itself never actually sleeps a negative amount (it
      re-clamps locally at its own point of use, per HAC-045/t/37).
      $api->retry->{delay} is now clamped to 0 the same way
      $api->retry->{count} already was (HAC-122, t/88)
    + fix: MANIFEST.SKIP's backup-file line was '\*\~', a regex requiring
      a literal '*' immediately before the '~' - it never matched an
      ordinary editor backup filename like "Client.pm~" or "foo.pl~" (the
      actual Emacs/vim convention), only the essentially impossible
      "literal asterisk right before a tilde" case. Live-verified: a
      planted lib/HTTP/API/Client.pm~ shipped straight into the dzil
      build tarball, unfiltered. Changed the pattern to '~$', which
      actually matches a trailing tilde (HAC-123, t/89)
    + test: coverage added for DEBUG_RESPONSE_HEADER_ONLY (previously
      untested anywhere - already correct, this closes the gap, HAC-124,
      t/90) and for kvp2str_each's plain-ARRAY branch filtering out a
      nested empty arrayref element (already correct, HAC-125, t/91)
    + cleanup: send()'s local negative-retry-delay re-clamp became dead
      code once HAC-122 added the same clamp to _build_retry() itself
      (which send() always calls fresh) - removed the now-unreachable
      line; t/37's end-to-end negative-RETRY_DELAY regression still
      passes, now exercised via _build_retry's clamp alone (HAC-126)
    + test: coverage added for kvp_response()'s empty-decoded_content
      early return (a real response with an empty body, distinct from
      t/09's earlier "no response at all" guard - already correct,
      HAC-127, t/92)
    + test: coverage added for _tune_utf8()'s silent-swallow-on-unrelated-
      error path (only "content must be bytes" gets re-encoded; any other
      error passes content through unchanged - already correct, tested
      via the same monkeypatch/white-box approach t/26 already
      established, HAC-128, t/93)

2026-08-19 - v1.24
    + docs: added POD ATTRIBUTES entries for "retry" and "debug_flags" -
      both are public read-write constructor attributes that had no
      documentation at all, the same gap class HAC-052 already fixed
      for retry_config/json. retry's entry carries the same warning
      already present in its in-code comment (it is a one-time snapshot,
      not a live view of send()'s actual retry behavior, despite the
      naming symmetry with retry_config). A new regression test
      (t/79) cross-checks every declared attribute against the POD
      ATTRIBUTES section so this can't silently recur (HAC-101)
    + fix: kvp2str_each's CSV branch used the s///r non-destructive
      substitution modifier, which requires Perl 5.14+ - cpanfile
      declares a 5.010 minimum (and ships that floor to CPAN/
      cpantesters via META.json), so any install/test on a genuinely
      5.10-5.13.x Perl would hit a hard compile-time syntax error.
      Rewritten to the same copy-then-substitute idiom using only
      Perl-5.10-compatible syntax; output confirmed byte-identical
      (HAC-102)
    + cleanup: Client.pm's @EXPORT is now derived from
      DataTypeMarker.pm's @EXPORT instead of being a hand-copied,
      independently-maintained duplicate of it - DataTypeMarker.pm's
      SYNOPSIS promises "use HTTP::API::Client" re-exports everything
      it defines, but nothing previously enforced the two lists staying
      in sync. Behavior-neutral (both lists were already identical); a
      new regression test (t/80) guards against future drift (HAC-104)
    + fix: convert_data() and new_request()'s GET-restriction check
      required an EXACT match for a form-urlencoded content_type, while
      the JSON branch already matched leniently (any content_type
      containing "json", including a charset-qualified value - this
      module's own default). A form-urlencoded content_type with a
      trailing parameter, e.g. "application/x-www-form-urlencoded;
      charset=utf-8" (a valid Content-Type value, and the same pattern
      the JSON default uses), died instead of working. Both sites now
      accept an anchored prefix match (HAC-107)
    + fix: an xBOOLEAN()/xCSV() marker used as a header value (rather
      than in body data) silently stringified to Perl's default
      blessed-reference form ("BOOL=ARRAY(0x...)") instead of being
      unwrapped or dying - the same silent-corruption failure class
      HAC-020/HAC-093/HAC-096 already established this project must
      never do. new_request()'s header-application loop now dies
      naming the offending header key (HAC-109)
    + fix: _execute_callbacks() resolved sibling CODE values within the
      same %data (or %headers) hash in Perl's randomized hash key
      order - a callback reading another key's already-resolved value
      got either the resolved value or the raw, unresolved CODE ref,
      purely depending on the process's hash seed. The identical
      script, run repeatedly, produced two different outcomes with no
      error either way. Now sorts keys before iterating, so the same
      data structure resolves identically on every run; documented the
      resulting order in send()'s POD (HAC-110)
    + test: extracted the capture_stderr test helper (identically
      duplicated across t/16, t/66, t/77) into t/lib/CaptureStderr.pm,
      matching t/lib/FakeUA.pm's existing style. Test-only, zero
      behavior change (HAC-111)
    + test: added regression coverage for the documented
      before_sorting_keys-mutation-discarded-by-events->{keys}
      interaction - already correct, never previously exercised
      together in one test (HAC-112)
    + cleanup: 4 older die() messages ("Unsupported engine", "Unable to
      create a get request", "Unable to convert data for content_type",
      "Unable to convert nested hash value") now end with a trailing
      newline, suppressing Perl's redundant "at FILE line N" suffix -
      matching every other die() in the file. Message-text-only;
      behavior-neutral for every existing test (HAC-114)
    + meta: added [MetaResources] to dist.ini - META.json shipped with
      no repository, bugtracker, or homepage URL at all, so MetaCPAN
      had nothing to link back to this distribution's real GitHub repo
      with. Metadata-only, no runtime code affected (HAC-115)
    + fix: xBOOLEAN()/xCSV() blessed markers into bare, unqualified
      package names "BOOL"/"CSV" instead of namespaced ones - a real
      Perl class-name collision risk, since kvp2json_each/kvp2str_each
      identify these markers purely by a ref() string check. Verified
      live: an unrelated object merely blessed into a class also named
      "BOOL" crashed kvp2json with a confusing "Not an ARRAY reference"
      error instead of a clear diagnosis. Now bless into
      HTTP::API::DataTypeMarker::BOOL/::CSV, eliminating the collision
      risk - a deliberate, visible change to what ref() returns for
      objects from xBOOLEAN()/xCSV() (HAC-108)
    + docs: documented that kvp2json_each's undef handling differs by
      nesting depth - a top-level undef value is omitted, but a nested
      hash's undef sub-value stays as an empty JSON string. Already
      correct, deliberately kept as-is; only the POD was missing (HAC-105)
    + docs: narrowed send()'s POD claim that "none of %data/%headers/
      %events are mutated" - it's a shallow copy (deliberately, so
      xBOOLEAN(\$flag)'s live-tracking keeps working), so a callback that
      mutates a nested value in one of these still mutates the caller's
      original data structure. The POD now says so explicitly instead of
      overclaiming (HAC-106)
    + fix: prepare_request()'s username/password/auth_token checks used
      plain Perl truthiness ("if ($u || $p)"), which silently treats the
      valid, non-empty string "0" as unset - inconsistent with this
      module's own established length()-based emptiness convention
      (_defor). Verified live: username=>"0", password=>"0" (both
      explicitly set) sent no Authorization header at all; auth_token=>
      "0" had the identical bug. Both now check length() instead of
      truthiness (HAC-118)
    + meta: added MIME::Base64 to cpanfile's test-phase requires - used
      by 4 test files (since t/02, predating this session) but never
      declared (HAC-119)
    + meta: also added Test::More (used by all 87 test files),
      FindBin (10 files), and File::Temp (t/83) to cpanfile's
      test-phase requires - a fuller follow-up check after HAC-119
      caught only MIME::Base64 (HAC-120)

2026-08-19 - v1.23
    + cleanup: removed the trailing "return $v" (and its now-redundant
      "if ref $v" guard) from kvp2json_each and kvp2str_each - a direct
      follow-up to HAC-096's die guard, which left both lines provably
      unreachable (every non-reference value already returns earlier,
      and any remaining reference is now always caught by the die).
      Zero behavior change; confirmed by an identical full-suite run
      before and after, and by Devel::Cover no longer listing either
      line as permanently-0%-executed dead weight (HAC-098)
    + test: added regression coverage for send()'s "-- RETRY N of M"
      debug line, only reachable when a debug flag is on AND an actual
      retry attempt happens - no existing test combined both. Already
      correct behavior; Client.pm now reaches 100% statement coverage
      (HAC-099)
    + test: added regression coverage for new_request() skipping
      _tune_utf8 when charset is not "utf8" - already correct behavior,
      never exercised through a real send()/post() call before this
      (HAC-100)

2026-08-19 - v1.22
    + fix: kvp2str_each silently stringified a bare (not xBOOLEAN-wrapped)
      SCALAR ref data value as "SCALAR(0x...)" instead of dying - the
      same silent-corruption failure class HAC-020 (nested hash) and
      HAC-093 (xBOOLEAN's own non-scalar-ref guard) already established
      this project must never do. kvp2json_each's equivalent fallback
      technically died too, but only by accident of JSON::XS itself
      refusing to encode an arbitrary scalar ref, with a message that
      never mentioned this module's own xBOOLEAN() vocabulary. Both
      encoders now die with the same clear message, naming the ref
      type and pointing at xBOOLEAN() (HAC-096)
    + test: added regression coverage for new_request()'s GET branch
      when the target URL already contains a '?' - the behavior was
      already correct but had zero test executions before this
      (HAC-097)

2026-08-19 - v1.21
    + fix: kvp2json/kvp2json_each silently emitted invalid JSON for a
      string data value that Scalar::Util::looks_like_number() recognizes
      as a non-finite number - exact casings "NaN", "Inf", "-Inf" (whose
      Perl NV-to-string roundtrip matches the original string
      byte-for-byte, satisfying the existing lossless-numify check meant
      for ordinary numeric strings like "5") were numified into a real
      Perl NaN/Infinity NV, which this module's utf8-mode JSON::XS
      encoder then wrote as the bareword token nan/-nan/inf/-inf instead
      of a quoted string - not valid JSON, undecodable by any standard
      JSON parser including JSON::XS itself, with no error or warning.
      _numify_if_lossless() now leaves such a value as its original
      string via a POSIX::isnan/isinf guard, the same way any other
      non-lossless numeric string already stays a string (HAC-095)

2026-08-19 - v1.20
    + fix: prepare_request() unconditionally wrote an Authorization
      header from auth_token whenever set (and username/password
      weren't), even when the caller had already explicitly passed
      their own Authorization header (any casing) for that specific
      call - the explicit per-call value was silently discarded with no
      error or warning, reachable regardless of the header's casing
      since HTTP::Headers normalizes names case-insensitively while the
      underlying Perl hash keys don't. auth_token now only supplies a
      default when no Authorization header is already present (HAC-094)
    + fix: xBOOLEAN() wrapping a reference other than a scalar ref (an
      ARRAY or HASH ref) silently stringified to "ARRAY(0x...)"/
      "HASH(0x...)" in both kvp2json_each and kvp2str_each instead of
      dying with a clear message - the same failure class HAC-020
      already established this project must never do silently. Both
      encoders now die naming the offending ref type; a plain scalar
      and a genuine scalar ref (xBOOLEAN's two documented input shapes)
      are unaffected (HAC-093)
    + fix: kvp2json_each's BOOL branch applied HAC-055/HAC-070/HAC-084's
      invalid-UTF-8-die and lossless-numify protection only to the LIVE
      scalar-ref case (xBOOLEAN(\$var)) - a PLAIN (non-ref) value, e.g.
      xBOOLEAN($value) called with a bare scalar, fell straight through
      to the raw unprotected value. A plain xBOOLEAN() holding invalid
      UTF-8 silently produced mojibake instead of dying, and a plain
      numeric-looking xBOOLEAN() value was never numified. Both are
      fully public, documented input shapes per xBOOLEAN's own POD ("a
      plain scalar or a scalar ref") - the same gap HAC-084 fixed for
      the live-ref case, missed for the plain-value case. The
      native-JSON-boolean behavior for a live ref holding exactly
      '0'/'1' (xTRUE/xFALSE's shape) is unaffected (HAC-092)
    + fix: kvp2json_each's BOOL branch decided whether to pass a live
      xBOOLEAN(\$var) scalar ref through unchanged to JSON::XS using
      NUMERIC equality ($$inner == 0 || == 1), but JSON::XS itself only
      accepts the exact canonical string/number 0 or 1. A live ref
      holding "01", "1.0", " 1", or "1e0" (all numerically 1, none
      canonically formatted) crashed with JSON::XS's raw "cannot encode
      reference to scalar ... unless the scalar is 0 or 1" instead of
      going through the numify/validate path every other live value
      already uses. Fixed by comparing with eq against '0'/'1' instead
      of == (HAC-090)
    + fix: kvp2json_each crashed on a live xBOOLEAN(\$var) reference
      holding undef - a regression introduced by HAC-084/HAC-090
      combining without an undef guard: $$inner eq '0'/'1' warned on
      undef, and since neither matched, the undef value reached
      _json_validate_utf8()/numify unguarded and died with a confusing
      "value is not valid UTF-8" error. Now defaults the live value to
      '' before the comparison, matching kvp2str_each's own undef
      handling (HAC-082) (HAC-091)
    + fix: send() merged pre_defined_data/pre_defined_headers/
      pre_defined_events directly into the caller's own
      \%data/\%headers/\%events hashrefs instead of copying them first,
      and new_request()/kvp2json()/kvp2str() autovivified
      before_header/after_header/not_include keys into the caller's
      actual \%events hashref even when never set. A caller reusing a
      hashref across multiple calls (a natural pattern) got silently
      corrupted state back. send() now shallow-copies all three
      immediately, before any merging or event processing happens
      (HAC-083)
    + fix: kvp2json_each's BOOL branch bypassed HAC-055's invalid-UTF-8
      protection for a live xBOOLEAN(\$var) reference holding raw
      non-UTF8-valid bytes - it went straight to numification without
      the UTF-8 validate-or-die step the main scalar branch does,
      silently producing mojibake instead of dying with a clear error.
      Both paths now share a _json_validate_utf8() helper (HAC-084)
    + fix: DEBUG_RESPONSE_IF_FAIL's own POD says it narrows DEBUG_IN_OUT
      to only print on a failed response, but only the RESPONSE half of
      DEBUG_IN_OUT's output was actually narrowed - the REQUEST dump
      printed unconditionally whenever DEBUG_IN_OUT was set, regardless
      of whether the response succeeded. DEBUG_SEND_OUT (a separate
      flag, not mentioned in DEBUG_RESPONSE_IF_FAIL's POD) is
      deliberately left unnarrowed (HAC-085)
    + fix: RETRY_FAIL_STATUS (and the equivalent retry_config->{
      fail_status}) with an empty or whitespace-only entry (a doubled
      comma, a lone whitespace segment) created a bogus '' key in the
      retry-status match set. Harmless in practice, but inconsistent
      with how empty segments are now skipped elsewhere in this module
      (kvp_response, HAC-075). Empty entries are now skipped entirely
      (HAC-086)
    + test: xCSV() with zero elements emits "key=" (present, empty
      value) rather than being omitted like a top-level empty plain
      array is (HAC-065) - this was correct and intentional (no
      join-corruption risk to avoid the way a nested empty array has)
      but previously undocumented and untested (HAC-087)
    + docs: the retry attribute (and its _build_retry builder) has been
      disconnected from send()'s actual behavior since HAC-068 - send()
      calls _build_retry directly on every call rather than reading the
      memoized attribute, so retry_config changes take effect live, but
      $api->retry itself is just a one-time snapshot. Added a code
      comment to prevent this being mistaken for dead/reintroducible
      logic (HAC-088)
    + docs: refreshed README's coverage baseline, stale since v1.15 -
      Client.pm 98.9%/93.1%/80.9% (was 98.8%/92.8%/76.6%),
      DataTypeMarker.pm still 100% (HAC-089)

2026-08-18 - v1.19
    + fix: kvp2str_each's BOOL branch produced a spurious "Use of
      uninitialized value" warning when a live xBOOLEAN(\$var) reference
      currently held undef - the undef value reached string
      interpolation undefaulted. kvp2json_each already handled the
      identical input silently (encodes JSON null). kvp2str_each now
      defaults an undef live value to '' before escaping, matching the
      scalar branch's existing convention (HAC-082)

2026-08-18 - v1.18
    + fix: kvp2json_each's BOOL branch unconditionally returned the
      marker's inner value, which is only safe when that value is
      exactly \1 or \0 (xTRUE()/xFALSE()'s shape) - JSON::XS's own
      convention for a native boolean via a scalar ref. xBOOLEAN(\$flag)
      is documented as a general live-tracking mechanism, not restricted
      to 0/1 - for any other live value (e.g. xBOOLEAN(\$status) where
      $status holds "active"), kvp2json_each handed JSON::XS a raw
      scalar ref it cannot encode at all, dying with "cannot encode
      reference to scalar ... unless the scalar is 0 or 1". kvp2str_each
      already dereferenced this exact case correctly. kvp2json_each now
      matches it: a live ref currently holding exactly 0/1 still gets
      JSON::XS's native boolean treatment, any other live value
      dereferences and encodes as its own actual contents (HAC-081)

2026-08-18 - v1.17
    + docs: kvp2str's before_sorting_keys/after_sorting_keys events are
      real, working, and tested (t/21_before_events.t,
      t/33_before_sorting_keys_mutation.t) but were never mentioned in
      the METHODS POD, which only documented the shared events->{keys}
      callback both encoders use. Documented both hooks: kvp2str-only,
      genuinely live (a callback mutating @$keys changes the actual
      output), before_sorting_keys' mutation only survives if
      events->{keys} isn't also set (HAC-079)
    + refactor: send()'s outer $retry (a settings hashref: count/status/
      delay) was immediately shadowed by the RETRY loop's own "foreach
      my $retry (0..$retry_count)" loop counter. Currently harmless -
      the hashref is fully consumed into separate lexicals before the
      loop starts - but a real trap for any future edit inside the loop
      expecting $retry->{...}. Renamed the outer variable to
      $retry_settings; no behavior change (HAC-080)

2026-08-18 - v1.16
    + fix: kvp2str_each's CSV branch always prefixed "$k=" regardless of
      %options' no_key flag, unlike the scalar/BOOL/ARRAY branches which
      all respect it. An xCSV value nested inside another xCSV is
      recursed into with no_key => 1 (the same mechanism HAC-071 fixed
      for a BOOL nested in CSV), but the CSV branch ignored it -
      xCSV(6, 7, xCSV(13, 14), 15) encoded to "e=6,7,e=13,14,15" (a
      bogus embedded "e=") instead of "e=6,7,13,14,15". CSV nested in
      CSV was the one nesting combination HAC-069/071 didn't cover
      (HAC-078)

2026-08-18 - v1.15
    + fix: kvp_response() only decoded %XX percent-encoding, never the
      application/x-www-form-urlencoded convention of a literal +
      meaning a space - decoding "q=hello+world" returned
      { q => "hello+world" } instead of { q => "hello world" }.
      kvp2str_each's own encoder never emits a raw + (it uses %20), so
      this only bit when kvp_response decoded a response body from any
      other API using the + convention, which is extremely common. A
      genuinely percent-encoded literal + (%2B) still decodes correctly,
      distinct from a raw + meaning space (HAC-074)
    + fix: kvp_response() produced a bogus '' => undef entry plus two
      uninitialized-value warnings whenever the response body had an
      empty &-separated segment (a leading, trailing, or doubled &,
      e.g. "a=1&&a=2") - split /&/ yields an empty string for that
      segment, and splitting *that* on = leaves both key and value
      undef. Same failure signature HAC-060/061/065 already fixed on
      the encode side, now closed on the decode side too - an empty
      segment is skipped entirely (HAC-075)
    + fix: json_response() never re-applied charset before decoding,
      unlike kvp2json() (HAC-067) which does on the encode side - json
      is lazy and memoized, built once in whatever charset mode was set
      at the time. Changing charset afterward and calling
      json_response() again on the same response body still decoded
      through the stale mode, silently producing mojibake for a
      UTF-8-encoded body if json had first been built in a non-utf8
      mode. json_response() now re-applies charset on every call,
      matching kvp2json()'s existing behavior (HAC-076)
    + fix: xCSV()/xBOOLEAN() both blessed \@_ directly - a reference to
      Perl's actual arguments array, which aliases the caller's
      variables rather than copying their values. A marker built inside
      a loop from a shared, reused scalar variable (a natural pattern)
      silently ended up holding whatever value that variable had LAST,
      not the value at the time each marker was created - e.g.
      xCSV($x, $y, 3) then reassigning $x/$y afterward changed the
      already-created marker's contents. Both constructors now bless a
      fresh array (bless [ @_ ], ...), copying each value at call time;
      explicitly passing a scalar ref (xBOOLEAN(\$flag)) still tracks
      live, which remains the documented way to opt into that (HAC-077)

2026-08-18 - v1.14
    + fix: an empty ARRAY nested inside an xCSV(...) list left a stray
      blank comma segment instead of being omitted - xCSV(6, 7, [], 15)
      encoded to "e=6,7,,15" (a double comma) rather than "e=6,7,15".
      kvp2str_each's ARRAY branch already returns '' for an empty array,
      the same string a genuine empty-string scalar CSV element also
      produces, so the CSV branch's loop couldn't tell them apart and
      pushed both into the comma-joined list the same way. HAC-065
      already established that an empty array is omitted entirely at
      the top level of kvp2str - this extends the same treatment to an
      empty array nested inside xCSV, without touching the genuinely
      different case of a real empty-string element (HAC-069)
    + fix: kvp2json_each/kvp2str_each both unconditionally coerced any
      numeric-looking string value via looks_like_number($v) ? $v+0 : $v
      - a leading-zero value like a US zip code "00501" silently became
      501 in both JSON and form-urlencoded output. kvp2json_each now
      only numifies when the round-trip is lossless (stringifying the
      numified value reproduces the original exactly), so "5" still
      becomes a real JSON number but "00501"/"5.0"/"+5" stay strings.
      kvp2str_each's numification is removed entirely - a query string
      has no separate number/string type, so it only ever risked
      corrupting the original representation for no benefit (HAC-070)
    + fix: kvp2str_each's BOOL branch ignored %options' no_key flag
      entirely and always prefixed "$k=" onto its output, unlike the
      scalar and ARRAY branches which respect it. A BOOL marker
      (xTRUE/xFALSE) nested inside an xCSV(...) list is recursed into
      with no_key => 1 like any other CSV element, but the leaked
      prefix corrupted the comma-joined string - xCSV(6, 7, xTRUE(), 15)
      encoded to "e=6,7,e=1,15" (a bogus embedded "e=") instead of
      "e=6,7,1,15". Now respects no_key the same way the scalar branch
      does (HAC-071)
    + fix: kvp2json_each's HASH branch never passed the nested hash's
      own key into %o, so a CODE-valued callback nested inside a hash
      saw its *outer* field's key instead of its own - the same class
      of bug HAC-064 fixed at the top level, one recursion level deeper,
      in a case HAC-064's own test never reached (HAC-072)
    + test: kvp2str_each's HASH branch already dies with a clear message
      instead of recursing (a query string has no standard convention
      for a nested hash, unlike kvp2json_each which recurses since JSON
      has a native object type) - this was correct but undocumented and
      untested, a third asymmetry alongside the two the METHODS POD
      already called out (HAC-073)

2026-08-18 - v1.13
    + fix: kvp2str_each returned an empty string for a top-level
      empty-arrayref-valued field instead of omitting it, and kvp2str's
      caller loop pushed that empty string in among the other encoded
      parts - { a => 1, tags => [], z => 9 } encoded to "a=1&&z=9" (a
      stray double ampersand). Decoding that back through this module's
      own kvp_response() produced a bogus '' => undef key and an
      uninitialized-value warning - the same failure signature HAC-061
      already fixed for a different trigger (an ARRAY nested inside
      xCSV). An empty array is now omitted entirely, the same treatment
      a key mapped to undef/missing already gets (HAC-065)
    + fix: browser_id/timeout/ssl_verify are documented as plain
      read-write attributes, but ua is lazy and memoized - built once,
      on first use, from whatever those attributes were at the time.
      Changing any of them after the first request was a silent no-op:
      the underlying LWP::UserAgent's agent/timeout/ssl_opts never
      updated, with no error or warning. send() now re-applies all
      three to the existing ua object at the start of every call, each
      guarded by a can() check so FakeUA and other minimal test doubles
      aren't broken (HAC-066)
    + fix: same class of bug as HAC-066, in the json attribute - lazy
      and memoized, built once from whatever charset was set at the
      time. Changing charset after the first JSON encode was a silent
      no-op: an invalid charset no longer triggered HAC-046's documented
      immediate die, and switching between two valid charsets had no
      effect on the actual output bytes. kvp2json() now re-applies
      charset to the existing json object at the start of every call
      (HAC-067)
    + fix: same class of bug as HAC-066/HAC-067, in retry_config - retry
      is lazy and memoized from _build_retry, built once from whatever
      retry_config was at the time send() first ran. Changing
      retry_config after that first call was a silent no-op: send() kept
      using the stale memoized retry count/delay/status on every later
      call. send() now recomputes retry fresh from the current
      retry_config at the start of every call (HAC-068)

2026-08-18 - v1.12
    + docs: DataTypeMarker.pm's DESCRIPTION and Client.pm's kvp2json POD
      both claimed kvp2json_each special-cases xCSV the same way
      kvp2str_each does. It doesn't - a CSV-blessed arrayref satisfies
      Perl's reftype-based ARRAY check regardless of blessing, so it
      falls through to the generic array branch and JSON-encodes as a
      plain array (xCSV(1,2,3) becomes [1,2,3]), which is the natural
      JSON representation anyway - form-urlencoded's one-key-per-element
      problem that CSV's comma-join solves doesn't exist in JSON. Only
      BOOL markers are genuinely special-cased by both encoders. Fixed
      the docs to match the (correct) actual behavior rather than
      changing working code to match an overclaim (HAC-063)
    + fix: kvp2json() never passed a top-level field's own key into %o
      for a CODE-valued field's callback, unlike kvp2str() which passes
      key => $k - a callback nested inside JSON-encoded data couldn't
      see its own key even though the identical callback could via the
      form-urlencoded path. Same class of two-encoders-drift as
      HAC-059/060/061, here in the callback mechanism rather than
      array/CSV encoding (HAC-064)

2026-08-18 - v1.11
    + fix: kvp2json_each silently substituted U+FFFD replacement
      characters into the JSON body for any data value that was raw
      bytes but NOT valid UTF-8 (e.g. Latin-1 data from a legacy
      file/DB), instead of erroring - now dies with a clear message
      naming the raw bytes involved rather than silently corrupting the
      value. The form-urlencoded encoder was never affected - only the
      JSON path assumed all non-UTF8-flagged byte strings were valid
      UTF-8 (HAC-055)
    + docs: fixed 'enviornment' typo in ENVIRONMENT VARIABLES POD
      (HAC-056)
    + docs: documented that kvp2json_each's HAC-055 invalid-UTF8 die is
      asymmetric with kvp2str_each, which has no such restriction for
      the same input (HAC-057)
    + test: added coverage for new_request()'s GET-with-wrong-content_type
      die, previously reachable but never exercised by the suite. The
      sibling die in convert_data() was already covered by
      t/10_unsupported_content_type.t (HAC-058)
    + fix: kvp2str_each double-escaped the key of any ARRAY-valued field
      whose key contained a character uri_escape() touches (space, &, =,
      %, non-ASCII) - { "a b" => ["x","y"] } produced "a%2520b=..."
      instead of "a%20b=...", corrupting the outgoing query string. Caused
      by the ARRAY (and CSV, though masked there by no_key short-
      circuiting before it could surface) branch recursing with its own
      already-escaped key instead of the raw one, so the next call's
      unconditional escape ran twice. Existing tests never caught it
      because none of their array/CSV-valued keys contained an escapable
      character (HAC-059)
    + fix: kvp_response() silently collapsed a repeated query-string key
      to its last value - decoding "tags=a&tags=b&tags=c" (exactly the
      shape kvp2str_each's ARRAY branch produces when encoding an
      array-valued field) returned { tags => 'c' }, losing 'a' and 'b'
      with no warning. Repeated keys now decode to an arrayref of every
      value seen, in order; a singleton key still decodes to a plain
      scalar, unchanged (HAC-060)
    + fix: kvp2str_each double-ampersanded an ARRAY value nested inside
      an xCSV(...) list - xCSV(6, 7, [13, 14], 15) encoded to
      "e=6,7,15&&e=13&e=14" (double &). Decoding that back through this
      module's own kvp_response() produced a bogus '' => undef key and
      uninitialized-value warnings - a round-trip data-integrity bug.
      t/05_kvp.t's existing expected-string literal had the double-&
      baked in as "expected" rather than catching it (HAC-061)
    + fix: retry_config set directly with only some keys (e.g. { delay =>
      10 }) left the omitted keys undef instead of falling back to their
      documented defaults - an omitted fail_response then hit send()'s
      own safety-net default of 1, silently turning "0 retries" into 1,
      and an omitted fail_status produced a spurious "Use of
      uninitialized value" warning from split(). _build_retry now
      defaults each key the same way the env-var path already did
      (HAC-062)

2026-08-18 - v1.10
    + docs: rewrote the USAGE section - the signed-request example (the
      module's stated reason to exist per DESCRIPTION) now leads instead
      of being buried after three generic examples, leftover casual-era
      phrasing from the original docs was removed, and the section was
      split into labeled subsections instead of one run-on block mixing
      old and new writing styles (HAC-054)

2026-08-18 - v1.09
    + docs: added ATTRIBUTES entries for retry_config and json - real,
      working programmatic alternatives to the env-var-only retry and
      JSON encoding config, previously undocumented (HAC-052)
    + docs: added a DESCRIPTION section explaining why this module
      exists (repetitive signed-API-request boilerplate) and when
      LWP::UserAgent/HTTP::Tiny are simpler choices instead, plus a
      verified-working signed-request USAGE example showing the event
      system computing a signature header while keeping the secret out
      of the body. Both README.md files gained a matching purpose
      paragraph before their setup instructions (HAC-051)

2026-08-18 - v1.08
    + docs: fixed 5 confirmed POD/README defects found by an explicit
      audit - the post() USAGE example said 'same as send(GET,...)'
      instead of POST, HTTP_TIMEOUT still had an unfilled '???'
      placeholder, browser_id/ua were undocumented in ATTRIBUTES, and
      src/README.md directly contradicted itself ("Docker only... no
      other supported dev setup" immediately followed by full local Perl
      instructions) with a stale coverage-baseline date (HAC-050)
    + fix: browser_id's version fallback was -1, producing the
      nonsensical User-Agent "HTTP API Client v-1" - $VERSION is only
      set by Dist::Zilla's [PkgVersion] plugin at build time, so this
      fallback fired for every non-CPAN-installed usage (a git checkout,
      including this project's own dev/test environment). Now falls back
      to "dev" instead (HAC-049)
    + docs: RETRY VARIABLES POD had a leftover "???" placeholder never
      filled in for RETRY_DELAY's default and a typo ("resposne") on
      RETRY_FAIL_RESPONSE - fixed, and documented the HAC-044/045
      negative-value clamping (HAC-048)
    + docs: charset attribute POD now names valid values and the
      HAC-046 invalid-value error behavior (HAC-047)
    + fix: an invalid charset value (e.g. a typo in HTTP_CHARSET) was
      silently swallowed by _build_json's eval, leaving JSON encoding
      without byte-encoding forced - a JSON request with any non-ASCII
      data then crashed much later with the confusing, unrelated
      "HTTP::Message content must be bytes" instead of a clear error
      naming the actual bad charset. Now dies immediately and clearly
      (HAC-046)
    + fix: a negative RETRY_DELAY reached sleep() as-is, producing
      "sleep() with negative argument" on every retry - same class of
      missing-validation gap as HAC-044, just in the delay rather than
      the count. Now clamped to a minimum of 0 in send() (HAC-045)
    + fix: a negative retry count (e.g. RETRY_FAIL_RESPONSE=-1) made
      send()/get()/etc silently return undef - Perl's 0..N range is empty
      for a negative N, so the retry loop's body (which builds and sends
      the request) never ran at all, with no error or warning. Now
      clamped to a minimum of 0 in _build_retry, matching the documented
      "default 0 retry" floor (HAC-044)
    + test: t/08_retry.t also switched to the shared t/lib/FakeUA.pm
      fixture (HAC-042 follow-up) - it was a strict subset of the shared
      fixture's behavior. No behavior change (HAC-043)
    + test: t/11_retry_fail_status.t and t/24_retry_fail_status_whitespace.t
      duplicated an identical FakeUA fixture verbatim - extracted into a
      shared t/lib/FakeUA.pm. No behavior change (HAC-042)
    + fix: send() defaulted $data/$headers/$events when omitted but not
      $path - calling get()/post()/etc with no path argument (a plausible
      pattern when base_url is meant to be the whole target URL) produced
      a spurious "Use of uninitialized value $path" warning. Now defaults
      $path via _defor(), matching the other three optional args (HAC-041)
    + refactor: kvp2json and kvp2str duplicated the exact same skip-key
      guard verbatim - the same shape of duplication that caused HAC-020
      (the two encoders silently drifting out of sync). Extracted into a
      shared _should_skip_key() helper. No behavior change (HAC-040)
    + test: coverage added for json_response()'s documented "no request
      made yet" behavior (never exercised before) - confirms it already
      matches the POD (HAC-039)

2026-08-17 - v1.07
    + docs: added POD for get_content_type/kvp2json_each/kvp2str_each,
      the only three public methods in Client.pm with none (Pod::Coverage
      82.3% -> 100%) (HAC-038)
    + fix: before_sorting_keys' keys parameter was always empty on entry,
      and any mutation a callback made to it (add/remove a key before
      sorting) was silently discarded a moment later when @keys got
      unconditionally reassigned from keys %data. Unlike after_sorting_keys,
      whose keys mutations are genuinely live, before_sorting_keys was
      functionally inert for this purpose (HAC-037)
    + fix: CPANTS Core Kwalitee was 93.75% (6 failing metrics) - added a
      LICENSE AND COPYRIGHT POD section to Client.pm, declared
      HTTP::Headers/HTTP::Request as runtime prereqs and HTTP::Request/
      HTTP::Response as test-phase prereqs in cpanfile (both used but
      previously undeclared), declared a minimum perl version, and added
      [MetaJSON] to dist.ini so the release includes META.json (HAC-036)
    + fix: Basic Auth (username/password) crashed outright on a wide
      Unicode username or password - authorization_basic()'s internal
      base64 encoding dies on a UTF8-flagged string, and unlike
      auth_token (fixed by HAC-034) username/password never went
      through any UTF-8 handling. Reuses _encode_if_utf8_flagged(),
      completing the sweep across every credential/header/body path
      (HAC-035)
    + fix: header values got no UTF-8 handling at all, unlike body values
      (fixed for body content just below by HAC-029/031/032) - a wide
      Unicode header value stayed UTF8-flagged all the way through,
      producing "Wide character in print" warnings and incorrect bytes
      when the request was serialized. New _encode_if_utf8_flagged()
      helper, shared with the body-encoding fixes, applied to header
      values before they reach HTTP::Request::header() (HAC-034)
    + fix: kvp2str_each's BOOL branch (xTRUE/xFALSE/xTrue/xFalse/xtrue/
      xfalse/xt__e/xf___e) interpolated its value directly into the
      query string with no percent-escaping at all, unlike every other
      branch. A value containing '&' or '=' - reachable since xBOOLEAN's
      own POD documents it as accepting any plain scalar, not just
      boolean-safe strings - corrupted the query string by introducing
      extra params (HAC-033)
    + fix: the JSON path (kvp2json/kvp2json_each) had the same class of
      bug as HAC-031, just below - JSON::XS's utf8 mode unconditionally
      re-encodes string values, which double-encodes a value that is
      already raw UTF-8 bytes, producing mojibake in the JSON body. Now
      decodes any non-UTF8-flagged string value before it reaches
      JSON::XS (HAC-032)
    + fix: HAC-029's uri_escape_utf8() fix (below, v1.06) double-encoded a
      value that was already raw UTF-8 bytes (utf8::is_utf8 false - the
      common shape for data read from a file/DB/API without being
      explicitly Encode::decode'd), producing mojibake instead of correct
      percent-encoding. A genuine Unicode character string still encodes
      correctly. New _uri_escape_bytes_or_chars() helper only encodes
      when the input is actually UTF8-flagged, mirroring _tune_utf8's own
      detect-then-encode approach (HAC-031)

2026-08-17 - v1.06
    + fix: any form-urlencoded request (the GET default, or content_type
      set explicitly) containing a genuinely wide Unicode character in a
      key or value - any CJK character, Cyrillic, Greek, emoji - crashed
      outright instead of encoding. kvp2str_each used URI::Escape's
      uri_escape(), which only handles codepoints up to 0xFF; switched to
      uri_escape_utf8(). JSON requests were unaffected (HAC-029)
    + test: coverage added for _tune_utf8's UTF-8 encoding path, tested
      directly as it's unreachable through the public API - convert_data()
      always hands it already byte-encoded content (HAC-028)
    + fix: RETRY_FAIL_STATUS silently dropped any status code after a
      comma-space separator (e.g. "500, 404") - the split didn't trim
      whitespace, so the leading space left on every status but the first
      never matched the response code and that status silently never
      retried (HAC-027)
    + test: coverage added for skip_headers/skip_key (new_request/
      kvp2json/kvp2str), previously undocumented and, for skip_headers,
      untested (HAC-026)
    + test: coverage added for before_headers and before_sorting_keys/
      after_sorting_keys events (never exercised before) (HAC-024)
    + fix: add_headers_keys, following its own documented usage (mutate
      %headers as a side effect, then return the key), caused that key to
      be double-counted in new_request()'s @keys - before_header/
      after_header for that key fired twice instead of once (HAC-025)
    + test: coverage added for headers_keys/add_headers_keys/before_header/
      after_header events (never exercised before), and for auth_token
      including the documented username/password-wins precedence rule
      (HAC-021, HAC-023)
    + fix: the not_include event was silently ignored in form-urlencoded
      mode (kvp2str) - it only worked for JSON (kvp2json). A key explicitly
      excluded via not_include still leaked into a form-urlencoded request
      body (HAC-022)
    + fix: kvp2str_each() silently stringified a nested hash value as
      'HASH(0x...)' in the query string - now dies with a clear message
      naming the key, mirroring the same fix already applied to
      convert_data() (HAC-020)

2026-08-17 - v1.05
    + test: coverage added for the DEBUG_* env vars (never exercised before);
      clarified DEBUG_RESPONSE_IF_FAIL's POD - it only narrows DEBUG_IN_OUT/
      DEBUG_RESPONSE, it does nothing by itself (HAC-017, HAC-018)
    + fix: _execute_callbacks() used each() on the data/headers hash while
      callbacks could mutate that same hash - confirmed via Perl's own
      "each() after insertion" undefined-behavior warning. Now iterates a
      keys() snapshot instead (HAC-016)
    + fix: root Dockerfile never put lib/ on PERL5LIB, so docker run always
      failed at 'use HTTP::API::Client' - verified with a real build+run,
      all tests now pass in the container (HAC-013)
    + test: coverage added for put()/head()/delete() (never exercised before)
      and for json_response()/kvp_response()'s actual decode logic (only
      their empty-input guards had coverage) - no behavior changed, all
      confirmed already correct (HAC-014, HAC-015)
    + fix: a client configured with an engine other than LWP::UserAgent now
      dies with a clear message instead of crashing later with a confusing
      "is_success on an undefined value" - real custom-engine dispatch is
      still an open design question, not decided here (HAC-010)
    + fix: RETRY_FAIL_STATUS crashed (wrong method name, decode_content vs
      decoded_content) any time it was actually used - the body-pattern-match
      it was trying to do was never wired up either, retry now happens purely
      on status-code match as the POD has always documented (HAC-009)
    + fix: kvp_response() crashed if called before any request was made -
      now returns {} like json_response() already did (HAC-007)
    + fix: convert_data() silently stringified a data hashref as 'HASH(0x...)'
      for any content_type other than json/form-urlencoded - now returns an
      empty body for empty data, dies with a clear message otherwise (HAC-008)
    + fix: a non-GET request (POST/PUT/DELETE) with application/x-www-form-urlencoded
      content-type and empty data never built an HTTP::Request object and crashed
      in send() - now builds an empty-content request correctly (HAC-004)
    + fix: send() slept RETRY_DELAY seconds on every failed request even with
      RETRY_FAIL_RESPONSE=0 (the default, no retries) - now returns immediately
      when no retry attempt is left (HAC-006)
    + license changed to MIT
    + Devel::Cover wired up as a develop-phase dependency, coverage documented in README

2021-03-31 - v1.03
    + use lazy builder, so the sub classes can just overwrite the _build_ sub instead of using default => sub {}

2021-03-31 - v1.02
    + convert number in the request
    + added data type markers
        + json true  = xTRUE()
        + json false = xFALSE()
        + cgi param true  = xTRUE()  => "1"
        + cgi param false = xFALSE() => "0"
        + cgi param true  = xTrue()  => "True" 
        + cgi param false = xFalse() => "False"
        + cgi param true  = xtrue()  => "true" 
        + cgi param false = xfalse() => "false"
        + cgi param true  = xt__e()  => "t"    
        + cgi param false = xf___e() => "f"    
        + cgi param csv list = %a = (a => xCSV(1,2,3,4)) => "a=1,2,3,4"
                   otherwise = %b = (b => [1,2,3,4])     => "b=1&b=2&b=3&b=4"

2021-03-31 - v1.01
    + Enchance key value pairs representing on the cgi params

2021-03-31 - v1.0
    + improve readibility
    + improve the logic path
    + adding events to manipulate the logic flow
    + change some private methods to public methods
    + Change OOP Framework to Moo

2021-02-25 - v0.09
    + the data and header can be using callback function to make it more dynamic

2018-09-24 - v0.08 / v0.07
    + Bugfix pre defined headers and parameters

2017-08-10 - v0.06
    + You can pre defined parameters during object construction
    + You can pre defined headers during object construction

2015-01-20 - v0.04
    + Update POD
    + Remove unwanted perltidy message

2015-01-20 - v0.04
    + Fix test

2015-01-19 - 0.03
    + Add ENVIRONMENT VARIABLE usage

2015-01-18 - 0.02
    + Cleanup

2015-01-18 - 0.01
    + First version


2021-04-27 - v1.04
    + New event to not include keys that is defined in the request

    + Simplified the cpan module dep

    + Refresh the tests
