Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion boss.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "horse",
"description": "Horse web framework \u2014 CrossSocket-compatible fork",
"version": "3.1.102",
"version": "3.1.103",
"homepage": "https://github.com/freitasjca/horse",
"license": "MIT",
"mainsrc": "src/",
Expand Down
4 changes: 2 additions & 2 deletions doc/request-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ For the route declaration itself, see [Routing](./routing.md). For middleware th
|---|---|---|
| `Body` | `string` | Raw request body decoded as UTF-8. Idempotent — multiple reads return the same cached string. |
| `Body<T>` | generic | Returns `FBody as T` — used when middleware (e.g. `Jhonson`) parses the body into an object. |
| `Body(AObject)` | setter | Used by middleware to attach a parsed body object. Frees any previous value. |
| `Body(AObject)` / `Body(AObject, AOwnsBody)` | setter | Used by middleware to attach a parsed body object. With `AOwnsBody = True` (the default / 1-arg form) Horse owns and frees the object, freeing any previous owned value first. Transports whose body is a non-owning reference into a socket buffer (e.g. CrossSocket) pass `AOwnsBody = False` so `Clear` nils the reference without freeing it. |
| `Params` | `THorseCoreParam` | Route path parameters: `Req.Params['id']`. |
| `Query` | `THorseCoreParam` | URL query string: `Req.Query['name']`. |
| `Headers` | `THorseCoreParam` | Request headers: `Req.Headers['Content-Type']`. Case-insensitive lookup. |
Expand Down Expand Up @@ -94,7 +94,7 @@ THorse.Post('/upload',
var
Stream: TStream;
begin
Stream := Req.ContentFields.AsStream('file'); // file field
Stream := Req.ContentFields.Field('file').AsStream; // file field (text fields: Field('x').AsString)
try
Stream.SaveToFile('uploaded.bin');
Res.Send('Saved ' + IntToStr(Stream.Size) + ' bytes');
Expand Down
4 changes: 2 additions & 2 deletions doc/request-response.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Para a declaração da rota em si, veja [Roteamento](./routing.pt-BR.md). Para m
|---|---|---|
| `Body` | `string` | Corpo da requisição decodificado como UTF-8. Idempotente — múltiplas leituras retornam a mesma string em cache. |
| `Body<T>` | genérico | Retorna `FBody as T` — usado quando middleware (ex. `Jhonson`) parseia o body num objeto. |
| `Body(AObject)` | setter | Usado por middleware para anexar um objeto parseado. Libera o valor anterior. |
| `Body(AObject)` / `Body(AObject, AOwnsBody)` | setter | Usado por middleware para anexar um objeto parseado. Com `AOwnsBody = True` (o padrão / forma de 1 argumento) o Horse é dono do objeto e o libera, liberando antes qualquer valor anterior que lhe pertença. Transportes cujo corpo é uma referência não-proprietária para um buffer de socket (ex.: CrossSocket) passam `AOwnsBody = False` para que o `Clear` apenas anule a referência sem liberá-la. |
| `Params` | `THorseCoreParam` | Parâmetros de caminho: `Req.Params['id']`. |
| `Query` | `THorseCoreParam` | Query string: `Req.Query['name']`. |
| `Headers` | `THorseCoreParam` | Headers da requisição: `Req.Headers['Content-Type']`. Lookup case-insensitive. |
Expand Down Expand Up @@ -94,7 +94,7 @@ THorse.Post('/upload',
var
Stream: TStream;
begin
Stream := Req.ContentFields.AsStream('file'); // campo de arquivo
Stream := Req.ContentFields.Field('file').AsStream; // campo de arquivo (campos de texto: Field('x').AsString)
try
Stream.SaveToFile('uploaded.bin');
Res.Send('Salvo ' + IntToStr(Stream.Size) + ' bytes');
Expand Down
58 changes: 57 additions & 1 deletion src/Horse.Core.Param.pas
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ THorseCoreParam = class
private
FParams: THorseList;
FFiles: TDictionary<string, TStream>;
{ PATCH-PARAM-1 (resolves FOLLOW-UP-MEM-1) � streams whose ownership was
transferred to this param via AddStream(..., AOwnsStream=True). FFiles is
a non-owning lookup dictionary; FOwnedStreams owns the actual objects and
frees them on Clear (pool recycle) and Destroy. }
FOwnedStreams: TObjectList<TStream>;
FFields: TDictionary<string, THorseCoreParamField>;
FContent: TStrings;
FRequired: Boolean;
Expand All @@ -38,6 +43,7 @@ THorseCoreParam = class
procedure ClearFields;
public
function Required(const AValue: Boolean): THorseCoreParam;
procedure Clear;
function Field(const AKey: string): THorseCoreParamField;
function ContainsKey(const AKey: string): Boolean;
function ContainsValue(const AValue: string): Boolean;
Expand All @@ -49,7 +55,16 @@ THorseCoreParam = class
property Items[const AKey: string]: string read GetItem; default;
property Dictionary: THorseList read GetDictionary;

function AddStream(const AKey: string; const AContent: TStream): THorseCoreParam;
{ AddStream � store a stream-backed field (e.g. a multipart file upload),
retrievable via Field(AKey).AsStream.
AOwnsStream = False (default / 2-arg overload): the stream is owned
elsewhere (e.g. CrossSocket's THttpMultiPartFormData) � never freed
here. Preserves the historical behaviour for every existing caller.
AOwnsStream = True: ownership is transferred to this param � Clear and
Destroy free the stream. Used by the mORMot bridge, which synthesises
a TMemoryStream per file part with no other owner (PATCH-PARAM-1). }
function AddStream(const AKey: string; const AContent: TStream): THorseCoreParam; overload;
function AddStream(const AKey: string; const AContent: TStream; const AOwnsStream: Boolean): THorseCoreParam; overload;

constructor Create(const AParams: THorseList);
destructor Destroy; override;
Expand Down Expand Up @@ -77,6 +92,9 @@ destructor THorseCoreParam.Destroy;
FParams.Free;
FContent.Free;
ClearFields;
{ PATCH-PARAM-1 � free owned streams (no-op when none were transferred).
FFiles is non-owning, so order vs. FreeAndNil(FFiles) is irrelevant. }
FreeAndNil(FOwnedStreams);
FreeAndNil(FFiles);
inherited;
end;
Expand All @@ -87,6 +105,28 @@ function THorseCoreParam.Required(const AValue: Boolean): THorseCoreParam;
FRequired := AValue;
end;

{-----------------------------------------------------------------------------
Limpa valores e campos cacheados para reutilizar o objeto em outra requisicao.
-----------------------------------------------------------------------------}
procedure THorseCoreParam.Clear;
begin
FParams.Clear;

if Assigned(FContent) then
FreeAndNil(FContent);

ClearFields;

{ PATCH-PARAM-1 � free owned streams BEFORE clearing the (non-owning) FFiles
lookup dictionary, so a pooled context does not accumulate one leaked
TMemoryStream per multipart upload. }
if Assigned(FOwnedStreams) then
FOwnedStreams.Clear;

if Assigned(FFiles) then
FFiles.Clear;
end;

function THorseCoreParam.ContainsKey(const AKey: string): Boolean;
begin
Result := FParams.ContainsKey(AKey);
Expand Down Expand Up @@ -118,11 +158,27 @@ function THorseCoreParam.GetCount: Integer;
end;

function THorseCoreParam.AddStream(const AKey: string; const AContent: TStream): THorseCoreParam;
begin
{ Backward-compatible overload � non-owning (historical behaviour). }
Result := AddStream(AKey, AContent, False);
end;

function THorseCoreParam.AddStream(const AKey: string; const AContent: TStream; const AOwnsStream: Boolean): THorseCoreParam;
begin
Result := Self;
if not Assigned(FFiles) then
FFiles := TDictionary<string, TStream>.Create;
FFiles.AddOrSetValue(AKey, AContent);

{ PATCH-PARAM-1 � when ownership is transferred, track the stream so Clear and
Destroy free it. Guard against double-registration of the same instance. }
if AOwnsStream and Assigned(AContent) then
begin
if not Assigned(FOwnedStreams) then
FOwnedStreams := TObjectList<TStream>.Create(True {AOwnsObjects});
if FOwnedStreams.IndexOf(AContent) < 0 then
FOwnedStreams.Add(AContent);
end;
end;

function THorseCoreParam.NewField(const AKey: string): THorseCoreParamField;
Expand Down
66 changes: 46 additions & 20 deletions src/Horse.Request.pas
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ THorseRequest = class
FContentFields: THorseCoreParam;
FCookie: THorseCoreParam;
FBody: TObject;
FOwnsBody: Boolean;
FSession: TObject;
FOwnsSession: Boolean;
FSessions: THorseSessions;
Expand Down Expand Up @@ -81,7 +82,7 @@ THorseRequest = class
public
function Body: string; overload; virtual;
function Body<T: class>: T; overload;
function Body(const ABody: TObject): THorseRequest; overload; virtual;
function Body(const ABody: TObject; AOwnsBody: Boolean = True): THorseRequest; overload; virtual;
function Session<T: class>: T; overload;
function Session(const ASession: TObject; AOwnsSession: Boolean = False): THorseRequest; overload; virtual;
function Headers: THorseCoreParam; virtual;
Expand Down Expand Up @@ -122,9 +123,10 @@ THorseRequest = class
PATCH-REQ-2 � added Clear procedure
Reason: THorseContext.Reset recycles pooled objects between requests
without Free/Create overhead. Rules enforced:
� FBody � set to nil, NEVER freed (non-owning CrossSocket buffer ref)
� FBody � freed only when FOwnsBody=True (Jhonson-style owned objects);
set to nil when FOwnsBody=False (CrossSocket non-owning buffer ref)
� FBodyString � set to '' (cached decoded body; repopulated by MapBody)
� FSession � set to nil (stale session = wrong-request auth)
� FSession � freed when FOwnsSession=True; set to nil otherwise
� FWebRequest � set to nil (belongs to previous Indy context)
� param collections � cleared in place, objects reused
=========================================================================== }
Expand Down Expand Up @@ -225,12 +227,17 @@ function THorseRequest.Body: string;
Result := FWebRequest.Content;
end;

function THorseRequest.Body(const ABody: TObject): THorseRequest;
{ PATCH-REQ-11 � AOwnsBody controls whether Clear frees FBody on pool recycle.
Default True preserves upstream ownership semantics (Jhonson, etc.).
CrossSocket's MapBody passes False � the body is a non-owning reference
into CrossSocket's receive buffer that must never be freed by Horse. }
function THorseRequest.Body(const ABody: TObject; AOwnsBody: Boolean = True): THorseRequest;
begin
Result := Self;
if Assigned(FBody) then
FBody.Free;
FBody := ABody;
if FOwnsBody and Assigned(FBody) then
FreeAndNil(FBody);
FBody := ABody;
FOwnsBody := AOwnsBody;
end;

function THorseRequest.Body<T>: T;
Expand Down Expand Up @@ -298,13 +305,26 @@ constructor THorseRequest.Create;
procedure THorseRequest.Clear;
begin
FWebRequest := nil;
// FBody: non-owning reference into CrossSocket's socket buffer.
// Must be set to nil here. NEVER call FBody.Free � doing so corrupts
// the live TCP connection. The pool Reset sets FBody := nil before
// calling Clear, but we enforce the contract here as a safety net.
FBody := nil;
{ PATCH-REQ-11 � respect body ownership.
FOwnsBody=True - owned object (e.g. Jhonson JSON) � free it.
FOwnsBody=False - non-owning ref (CrossSocket buffer) � just nil. }
if FOwnsBody and Assigned(FBody) then
FreeAndNil(FBody)
else
FBody := nil;
FOwnsBody := False;

FBodyString := ''; { PATCH-REQ-9 }
FSession := nil;

{ PATCH-REQ-10 - libera sess�o owned antes de reutilizar o request.
No provider mORMot o THorseRequest � reaproveitado pelo pool; apenas
zerar FSession deixa o objeto anterior sem dono e causa vazamento. }
if FOwnsSession and Assigned(FSession) then
FreeAndNil(FSession)
else
FSession := nil;
FOwnsSession := False;

{ PATCH-REQ-3 � wipe shadow fields so next request starts clean }
FCSMethod := '';
FCSMethodType := mtAny;
Expand All @@ -318,11 +338,11 @@ procedure THorseRequest.Clear;
FreeAndNil(FCSRawWebRequest);
{ end PATCH-REQ-8 }
if Assigned(FHeaders) then
FHeaders.Dictionary.Clear;
FHeaders.Clear;
if Assigned(FQuery) then
FreeAndNil(FQuery);
if Assigned(FParams) then
FParams.Dictionary.Clear;
FParams.Clear;
if Assigned(FContentFields) then
FreeAndNil(FContentFields);
if Assigned(FCookie) then
Expand Down Expand Up @@ -350,7 +370,7 @@ destructor THorseRequest.Destroy;
FreeAndNil(FContentFields);
if Assigned(FCookie) then
FreeAndNil(FCookie);
if Assigned(FBody) then
if FOwnsBody and Assigned(FBody) then
FBody.Free;
if FOwnsSession and Assigned(FSession) then
FreeAndNil(FSession);
Expand Down Expand Up @@ -629,9 +649,15 @@ function THorseRequest.RawWebRequest: {$IF DEFINED(FPC)}TRequest{$ELSE}TWebReque
function THorseRequest.Session(const ASession: TObject; AOwnsSession: Boolean): THorseRequest;
begin
Result := Self;

if Assigned(FSession) then
FreeAndNil(FSession);

{ PATCH-REQ-10 - respeita a propriedade da sess�o anterior.
Se a sess�o anterior n�o era owned, n�o deve ser liberada aqui; se era
owned, precisa ser liberada antes de substituir a refer�ncia. }
if FOwnsSession and Assigned(FSession) then
FreeAndNil(FSession)
else
FSession := nil;

FSession := ASession;
FOwnsSession := AOwnsSession;
end;
Expand Down Expand Up @@ -672,7 +698,7 @@ procedure THorseRequest.Populate(
if not Assigned(FHeaders) then
FHeaders := THorseCoreParam.Create(THorseList.Create).Required(False)
else
FHeaders.Dictionary.Clear;
FHeaders.Clear;
end;

function THorseRequest.RemoteAddr: string;
Expand Down
5 changes: 4 additions & 1 deletion src/Horse.Response.pas
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,10 @@ constructor THorseResponse.Create(const AWebResponse: {$IF DEFINED(FPC)}TRespons
procedure THorseResponse.Clear;
begin
FWebResponse := nil;
FContent := nil;

if Assigned(FContent) then
FreeAndNil(FContent);

if Assigned(FCustomHeaders) then
FCustomHeaders.Clear;
{ PATCH-RES-4 � wipe CrossSocket shadow fields }
Expand Down