Skip to content

fix(util/http): default POST Content-Type to application/octet-stream#829

Merged
oskarszoon merged 1 commit into
bsv-blockchain:mainfrom
oskarszoon:fix/asset-binary-content-type
May 8, 2026
Merged

fix(util/http): default POST Content-Type to application/octet-stream#829
oskarszoon merged 1 commit into
bsv-blockchain:mainfrom
oskarszoon:fix/asset-binary-content-type

Conversation

@oskarszoon

Copy link
Copy Markdown
Contributor

Summary

util/http.go:DoHTTPRequest hardcoded Content-Type: application/json for every POST request body. The only internal caller that sends a body via this helper is subtreevalidation.getMissingTransactionsBatch, which streams packed 32-byte tx hashes — binary data, never JSON.

When that POST traverses a ModSecurity WAF (deployed in front of the mainnet asset ingress), the JSON request body parser is selected based on the header, fails on binary content, and Rule 200002 (Failed to parse request body) denies the request with HTTP 400 + an HTML error page.

The downstream effect is severe: each rejected getMissingTransactionsBatch call counts as a catchup failure against the source peer. Reputation math (successRate * 0.6 + baseScore * 0.4 - recentFailurePenalty) drives reputation toward 5.0 over time, eventually starving sync entirely. Observed in production on mainnet-eu-1 peers (mainnet.gorillanode.io, mainnet2.gorillanode.io, bsva-ovh-teranode-eu-3).

ModSecurity log excerpt:

Rule 200002 - Failed to parse request body.
data: "JSON parsing error: lexical error: invalid char in json text.\x0a"
Content-Type: application/json
Content-Length: 32
URI: /api/v1/subtree/{hash}/txs

Fix

Switch the default Content-Type from application/json to application/octet-stream. This matches every body shape the helper actually carries today. There are no JSON-body callers — if one is added later it should set the header explicitly rather than relying on the helper default.

Changes

  • util/http.go: change hardcoded Content-Type to application/octet-stream, document the WAF interaction in a comment so future readers don't revert it.
  • util/http_test.go: update the four POST test assertions that checked for the old header.

Test plan

  • go test ./util/ -run 'TestDoHTTPRequest' -count=1 — 29 passed
  • go vet ./util/... — clean
  • Verify on staging that getMissingTransactionsBatch no longer 400s when traversing a ModSecurity-fronted ingress
  • Watch peer reputation stays >50 over a sync cycle on mainnet-eu-1 after deployment

Notes

A complementary infra patch is being applied to the ModSecurity rule set on the affected ingress to add a path-scoped requestBodyAccess=Off for /api/v1/(subtree/{hash}/txs|tx|txs). That covers older peers still sending the wrong header until they upgrade past this PR.

DoHTTPRequest hardcoded Content-Type: application/json for any POST
body. The only internal caller that sends a body is
subtreevalidation.getMissingTransactionsBatch, which streams packed
32-byte tx hashes (binary). When that POST traverses a ModSecurity
WAF in front of asset, the JSON body parser fails on binary data and
Rule 200002 rejects the request with HTTP 400. Each rejection drops
the source peer's reputation; with the WAF in place across the
mainnet ingress the failure path eventually pushes peers below the
selection threshold and starves catchup.

Switch to application/octet-stream — matches every body shape this
helper actually carries today. JSON callers do not exist; if one is
added later it should set the header explicitly.
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


Current Review:

No issues found. The change is correct and well-justified:

  • Root cause analysis is accurate: The PR description correctly identifies that getMissingTransactionsBatch in services/subtreevalidation/SubtreeValidation.go:248 is the only internal caller sending a POST body through DoHTTPRequest, and it sends binary data (packed 32-byte tx hashes), not JSON.

  • Fix is appropriate: Changing the default Content-Type from application/json to application/octet-stream aligns with actual usage and prevents WAF rejection.

  • Tests updated correctly: All four POST test assertions in util/http_test.go now expect the new header.

  • Documentation is clear: The added comment in util/http.go:229-235 explains the WAF interaction and why this specific Content-Type is necessary, preventing future confusion.

  • Change is minimal: Only touches the necessary lines (header value + comment + test expectations).

@sonarqubecloud

sonarqubecloud Bot commented May 7, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-829 (d9d2b91)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 142
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.710µ 1.700µ ~ 1.000
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.74n 61.94n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.67n 61.66n ~ 0.800
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.74n 61.68n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 31.70n 31.20n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 54.75n 53.65n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 132.0n 110.9n ~ 0.100
MiningCandidate_Stringify_Short-4 266.7n 264.9n ~ 0.100
MiningCandidate_Stringify_Long-4 1.947µ 1.919µ ~ 0.100
MiningSolution_Stringify-4 990.8n 985.2n ~ 0.100
BlockInfo_MarshalJSON-4 1.804µ 1.798µ ~ 1.000
NewFromBytes-4 125.1n 148.7n ~ 0.200
Mine_EasyDifficulty-4 60.58µ 60.74µ ~ 0.400
Mine_WithAddress-4 6.807µ 6.753µ ~ 0.100
DiskTxMap_SetIfNotExists-4 3.566µ 4.043µ ~ 0.100
DiskTxMap_SetIfNotExists_Parallel-4 3.370µ 3.643µ ~ 0.100
DiskTxMap_ExistenceOnly-4 313.2n 456.9n ~ 0.100
Queue-4 193.4n 199.5n ~ 0.100
AtomicPointer-4 4.903n 4.972n ~ 0.700
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 937.4µ 940.5µ ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/10K-4 892.9µ 893.8µ ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/10K-4 125.2µ 123.2µ ~ 0.700
ReorgOptimizations/AllMarkFalse/New/10K-4 63.12µ 62.28µ ~ 0.100
ReorgOptimizations/HashSlicePool/Old/10K-4 71.38µ 72.33µ ~ 0.400
ReorgOptimizations/HashSlicePool/New/10K-4 11.98µ 11.72µ ~ 0.200
ReorgOptimizations/NodeFlags/Old/10K-4 5.689µ 5.577µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.839µ 1.973µ ~ 0.200
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 11.80m 11.55m ~ 0.200
ReorgOptimizations/DedupFilterPipeline/New/100K-4 11.24m 10.61m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.203m 1.209m ~ 0.700
ReorgOptimizations/AllMarkFalse/New/100K-4 687.8µ 682.5µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 639.5µ 642.5µ ~ 1.000
ReorgOptimizations/HashSlicePool/New/100K-4 319.4µ 285.8µ ~ 0.200
ReorgOptimizations/NodeFlags/Old/100K-4 58.46µ 58.63µ ~ 0.400
ReorgOptimizations/NodeFlags/New/100K-4 19.19µ 20.13µ ~ 0.100
TxMapSetIfNotExists-4 51.76n 51.53n ~ 0.100
TxMapSetIfNotExistsDuplicate-4 37.94n 38.20n ~ 0.700
ChannelSendReceive-4 595.4n 619.8n ~ 0.100
BlockAssembler_AddTx-4 0.02831n 0.02930n ~ 1.000
AddNode-4 11.44 11.84 ~ 0.100
AddNodeWithMap-4 12.40 12.05 ~ 0.700
DirectSubtreeAdd/4_per_subtree-4 75.74n 74.09n ~ 0.400
DirectSubtreeAdd/64_per_subtree-4 41.31n 41.16n ~ 1.000
DirectSubtreeAdd/256_per_subtree-4 39.99n 40.40n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 39.03n 38.91n ~ 1.000
DirectSubtreeAdd/2048_per_subtree-4 38.44n 38.60n ~ 0.400
SubtreeProcessorAdd/4_per_subtree-4 332.5n 338.2n ~ 0.400
SubtreeProcessorAdd/64_per_subtree-4 325.4n 327.9n ~ 0.400
SubtreeProcessorAdd/256_per_subtree-4 328.8n 323.9n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 318.1n 315.3n ~ 0.400
SubtreeProcessorAdd/2048_per_subtree-4 311.5n 322.5n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 317.3n 325.9n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 314.9n 322.7n ~ 0.400
SubtreeProcessorRotate/256_per_subtree-4 309.7n 319.1n ~ 0.200
SubtreeProcessorRotate/1024_per_subtree-4 315.3n 318.0n ~ 0.400
SubtreeNodeAddOnly/4_per_subtree-4 88.31n 88.98n ~ 0.700
SubtreeNodeAddOnly/64_per_subtree-4 65.21n 65.49n ~ 0.400
SubtreeNodeAddOnly/256_per_subtree-4 64.43n 64.55n ~ 0.200
SubtreeNodeAddOnly/1024_per_subtree-4 63.75n 64.16n ~ 0.600
SubtreeCreationOnly/4_per_subtree-4 147.2n 147.5n ~ 0.400
SubtreeCreationOnly/64_per_subtree-4 543.9n 554.3n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.935µ 1.920µ ~ 0.400
SubtreeCreationOnly/1024_per_subtree-4 6.371µ 6.231µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 11.68µ 11.66µ ~ 1.000
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 313.1n 310.1n ~ 1.000
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 309.6n 325.0n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 651.6µ 668.5µ ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 1.751m 1.796m ~ 0.200
ParallelGetAndSetIfNotExists/50k_nodes-4 8.852m 9.132m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 17.77m 18.28m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 697.6µ 690.2µ ~ 0.700
SequentialGetAndSetIfNotExists/10k_nodes-4 3.348m 3.368m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 12.74m 12.52m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 24.06m 24.20m ~ 1.000
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 736.4µ 736.6µ ~ 1.000
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 4.825m 4.775m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 21.43m 21.60m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 758.7µ 752.0µ ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 6.986m 7.010m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 47.34m 47.84m ~ 0.100
CalcBlockWork-4 504.8n 505.6n ~ 1.000
CalculateWork-4 692.5n 705.6n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.301µ 1.291µ ~ 0.200
BuildBlockLocatorString_Helpers/Size_100-4 12.41µ 12.28µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_1000-4 122.2µ 135.9µ ~ 1.000
CatchupWithHeaderCache-4 104.2m 104.2m ~ 1.000
_prepareTxsPerLevel-4 409.2m 406.8m ~ 0.700
_prepareTxsPerLevelOrdered-4 3.656m 3.928m ~ 0.700
_prepareTxsPerLevel_Comparison/Original-4 430.2m 429.1m ~ 1.000
_prepareTxsPerLevel_Comparison/Optimized-4 3.599m 3.523m ~ 0.200
_BufferPoolAllocation/16KB-4 3.585µ 3.436µ ~ 0.100
_BufferPoolAllocation/32KB-4 7.541µ 9.687µ ~ 0.700
_BufferPoolAllocation/64KB-4 18.90µ 15.68µ ~ 0.100
_BufferPoolAllocation/128KB-4 33.52µ 31.98µ ~ 0.100
_BufferPoolAllocation/512KB-4 121.2µ 120.4µ ~ 1.000
_BufferPoolConcurrent/32KB-4 18.09µ 19.86µ ~ 0.100
_BufferPoolConcurrent/64KB-4 28.38µ 30.67µ ~ 0.100
_BufferPoolConcurrent/512KB-4 180.4µ 183.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 716.0µ 728.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 687.3µ 716.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 687.4µ 712.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 686.3µ 704.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 702.9µ 711.2µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 38.17m 37.99m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/32KB-4 38.00m 38.16m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.87m 38.19m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/128KB-4 38.14m 38.23m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/512KB-4 38.12m 38.08m ~ 1.000
_PooledVsNonPooled/Pooled-4 818.1n 828.4n ~ 0.100
_PooledVsNonPooled/NonPooled-4 7.476µ 7.883µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 9.241µ 9.032µ ~ 0.700
_MemoryFootprint/Proposed_32KB_32concurrent-4 11.76µ 13.08µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 11.25µ 12.29µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.382m 1.371m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 314.4µ 320.1µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 78.01µ 78.18µ ~ 1.000
SubtreeSizes/10k_tx_256_per_subtree-4 19.46µ 19.40µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.724µ 9.652µ ~ 1.000
SubtreeSizes/10k_tx_1024_per_subtree-4 4.783µ 4.815µ ~ 0.400
SubtreeSizes/10k_tx_2k_per_subtree-4 2.404µ 2.405µ ~ 1.000
BlockSizeScaling/10k_tx_64_per_subtree-4 75.74µ 75.66µ ~ 1.000
BlockSizeScaling/10k_tx_256_per_subtree-4 19.34µ 19.57µ ~ 0.400
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.803µ 4.812µ ~ 1.000
BlockSizeScaling/50k_tx_64_per_subtree-4 398.3µ 388.4µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 95.20µ 96.40µ ~ 0.200
BlockSizeScaling/50k_tx_1024_per_subtree-4 24.33µ 24.12µ ~ 1.000
SubtreeAllocations/small_subtrees_exists_check-4 157.3µ 158.6µ ~ 0.400
SubtreeAllocations/small_subtrees_data_fetch-4 162.9µ 164.5µ ~ 0.200
SubtreeAllocations/small_subtrees_full_validation-4 318.6µ 317.8µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 9.756µ 9.651µ ~ 0.100
SubtreeAllocations/medium_subtrees_data_fetch-4 10.27µ 10.24µ ~ 0.200
SubtreeAllocations/medium_subtrees_full_validation-4 19.58µ 19.70µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.380µ 2.369µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.522µ 2.540µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 4.890µ 4.985µ ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 327.6µ 325.2µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 326.0µ 321.6µ ~ 1.000
GetUtxoHashes-4 271.6n 273.2n ~ 1.000
GetUtxoHashes_ManyOutputs-4 45.92µ 51.37µ ~ 0.100
_NewMetaDataFromBytes-4 230.5n 231.7n ~ 0.500
_Bytes-4 605.9n 615.8n ~ 0.100
_MetaBytes-4 574.5n 561.3n ~ 0.700

Threshold: >10% with p < 0.05 | Generated: 2026-05-07 18:23 UTC

Comment thread util/http.go
if len(requestBody) > 0 && requestBody[0] != nil {
req.Body = io.NopCloser(bytes.NewReader(requestBody[0]))
req.Method = http.MethodPost
req.Header.Set("Content-Type", "application/json")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WTF?

@oskarszoon oskarszoon merged commit 7f3a79a into bsv-blockchain:main May 8, 2026
25 checks passed
@oskarszoon oskarszoon deleted the fix/asset-binary-content-type branch May 8, 2026 09:50
ordishs added a commit to ordishs/teranode that referenced this pull request May 19, 2026
…efs #4571)

Add TestCheckBlockSubtrees_OversizedBody to cover the third peer-fetch
call site at check_block_subtrees.go:218, mirroring the existing
fetchSubtreeFromPeer and getSubtreeTxHashes coverage so all three peer-
fetch paths fail closed on oversized responses.

Align TestDoHTTPRequestBounded_POST content-type assertion with the
application/octet-stream default introduced upstream by bsv-blockchain#829, surfaced
after rebasing onto main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants