From aecccd0c416af2e09bbfec76a63ecf82b75852c6 Mon Sep 17 00:00:00 2001 From: Norbert Hartl Date: Wed, 16 Sep 2026 12:27:21 +0200 Subject: [PATCH] Write a request snapshot to a byte stream ZnMessage>>writeOn: writes bytes, but withStreamDo: opened the file with writeStream, a character stream with a UTF-8 encoder in front of it. Every byte above 127 was therefore encoded a second time: the C3 9F of an "ss" became C3 83 C2 9F, and a snapshot came out longer than the Content-Length it carried. A captured request from a German tenant read "FaaAYen" where the client had sent "Faassen", which is a good way to send someone looking for an encoding bug that is not there. The stack trace branch wrote text to that same stream, so it now encodes the trace itself before handing it over. The handler hard-codes /data/logs, so the test reaches it through a subclass that answers a directory of its own rather than opening up the production class. Co-Authored-By: Claude Opus 5 (1M context) --- .../HOPSnapshotRouteHandlerTest.class.st | 45 +++++++++++++++++++ .../HOPTestSnapshotRouteHandler.class.st | 21 +++++++++ .../HOP-Core/HOPSnapshotRouteHandler.class.st | 13 ++++-- 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 source/HOP-Core-Tests/HOPSnapshotRouteHandlerTest.class.st create mode 100644 source/HOP-Core-Tests/HOPTestSnapshotRouteHandler.class.st diff --git a/source/HOP-Core-Tests/HOPSnapshotRouteHandlerTest.class.st b/source/HOP-Core-Tests/HOPSnapshotRouteHandlerTest.class.st new file mode 100644 index 0000000..06c8a05 --- /dev/null +++ b/source/HOP-Core-Tests/HOPSnapshotRouteHandlerTest.class.st @@ -0,0 +1,45 @@ +Class { + #name : 'HOPSnapshotRouteHandlerTest', + #superclass : 'TestCase', + #instVars : [ + 'uriSpace', + 'directory' + ], + #category : 'HOP-Core-Tests-Tests', + #package : 'HOP-Core-Tests', + #tag : 'Tests' +} + +{ #category : 'running' } +HOPSnapshotRouteHandlerTest >> setUp [ + super setUp. + directory := (FileLocator temp / ('hop-snapshot-', UUID new asString36)) ensureCreateDirectory. + uriSpace := (HOPUriSpace /+ #scim) root. + uriSpace / #scim @@ (#POST -> (HOPPluggableHandler new block: [ :req | #ok ])). + uriSpace addHandler: (HOPTestSnapshotRouteHandler new path: directory; yourself) +] + +{ #category : 'running' } +HOPSnapshotRouteHandlerTest >> tearDown [ + directory ifNotNil: [ directory ensureDeleteAll ]. + super tearDown +] + +{ #category : 'tests' } +HOPSnapshotRouteHandlerTest >> testSnapshotKeepsTheBytesOnTheWire [ + "Der Mitschnitt gibt die Bytes des Requests unveraendert wieder. Auf einem + Zeichen-Strom kodiert der UTF-8-Encoder jedes Byte ueber 127 ein zweites + Mal - aus C3 9F wird C3 83 C2 9F, und der Rumpf enthaelt dann nicht mehr, + was der Client geschickt hat." + | body request snapshot | + body := '{"familyName":"Faaßen"}'. + request := (ZnRequest post: #scim) + entity: (ZnEntity with: body type: ZnMimeType applicationJson); + yourself. + uriSpace asZincDelegate handleRequest: request. + snapshot := ZnCharacterEncoder utf8 decodeBytes: + (directory files detect: [ :each | each basename beginsWith: 'SCIM-' ]) binaryReadStream upToEnd. + self + assert: (snapshot includesSubstring: body) + description: 'Der Mitschnitt gibt den Rumpf nicht Byte fuer Byte wieder' +] diff --git a/source/HOP-Core-Tests/HOPTestSnapshotRouteHandler.class.st b/source/HOP-Core-Tests/HOPTestSnapshotRouteHandler.class.st new file mode 100644 index 0000000..03cff77 --- /dev/null +++ b/source/HOP-Core-Tests/HOPTestSnapshotRouteHandler.class.st @@ -0,0 +1,21 @@ +Class { + #name : 'HOPTestSnapshotRouteHandler', + #superclass : 'HOPSnapshotRouteHandler', + #instVars : [ + 'testPath' + ], + #category : 'HOP-Core-Tests-Tests', + #package : 'HOP-Core-Tests', + #tag : 'Tests' +} + +{ #category : 'accessing' } +HOPTestSnapshotRouteHandler >> path [ + "Statt /data/logs, damit ein Test den Mitschnitt wieder einlesen kann." + ^ testPath +] + +{ #category : 'accessing' } +HOPTestSnapshotRouteHandler >> path: aFileReference [ + testPath := aFileReference +] diff --git a/source/HOP-Core/HOPSnapshotRouteHandler.class.st b/source/HOP-Core/HOPSnapshotRouteHandler.class.st index d365241..e8faf12 100644 --- a/source/HOP-Core/HOPSnapshotRouteHandler.class.st +++ b/source/HOP-Core/HOPSnapshotRouteHandler.class.st @@ -15,10 +15,10 @@ HOPSnapshotRouteHandler >> handleRoute: aRoute [ | response | (self loggingEnabled: aRoute) ifFalse: [ ^ super handleRoute: aRoute ]. - self withStreamDo: [ :stream |. + self withStreamDo: [ :stream | aRoute request writeOn: stream. stream - nextPutAll: String crlf; + nextPutAll: String crlf utf8Encoded; flush. [ [ @@ -30,7 +30,8 @@ HOPSnapshotRouteHandler >> handleRoute: aRoute [ ] on: Error do: [ :error | - error signalerContext printDebugStackOn: stream. + stream nextPutAll: (String streamContents: [ :out | + error signalerContext printDebugStackOn: out ]) utf8Encoded. stream flush. error pass ] ] @@ -54,8 +55,12 @@ HOPSnapshotRouteHandler >> path [ { #category : 'as yet unclassified' } HOPSnapshotRouteHandler >> withStreamDo: aBlock [ + "Ein Byte-Strom, denn ZnMessage>>writeOn: schreibt Bytes. Auf einem + Zeichen-Strom kodiert der UTF-8-Encoder jedes Byte ueber 127 ein zweites + Mal: aus C3 9F wird C3 83 C2 9F, aus 'Faassen' wird 'FaaAYen'. Der + Mitschnitt wuerde dann laenger als sein eigenes Content-Length." | stream | - stream := self filename writeStream. + stream := self filename binaryWriteStream. [ aBlock value: stream ] ensure: [ stream