![]() |
REST Umsetzung eines CURL Befehls nach Delphi
Hallo.
Ich habe eine Problem dass mich schon seit 2 Tagen verfolgt. Und zwar muss ich folgenden CURL-Befehl zur Nutzung einer API in Delphi umsetzen welcher mehrere Dateien über ein "files" array überträgt:
Code:
Der CURL-Befehl funktioniert. Die Dateien liegen vor.
curl -X 'POST' \
'https://<provider>/Documents/Outbox' -H 'accept: application/json' -H 'X-Api-Key: My-API-Key' -H 'Content-Type: multipart/form-data' -F 'files=@testFile.pdf;type=application.pdf' -F 'files=@testFile.xml;type=text/xml' Ich habe es per RESTClient wie auch Indy versucht. Andere Befehle welche per GET JSON Daten austauschen funktionieren einwandfrei. Jedoch beim POST erhalte ich immer nur ein http/400. Indy:
Code:
RESTClient:
procedure SendFilesIndy(const PdfFile, XmlFile: string);
var HTTP: TIdHTTP; FormData: TIdMultiPartFormDataStream; Response: string; begin HTTP:= TIdHTTP.Create(nil); FormData:= TIdMultiPartFormDataStream.Create; try HTTP.Request.CustomHeaders.AddValue('Accept', 'application/json'); HTTP.Request.CustomHeaders.AddValue('X-Api-Key', 'My-API-Key'); HTTP.Request.CustomHeaders.AddValue('Content-Type', 'multipart/form-data'); AttachSSLHandler(HTTP, [sslvTLSv1_2]); // Setzt IOHandler if PdfFile<>'' then FormData.AddFile('files', PdfFile, GetMIMETypeFromFile(PdfFile)); // FormData.AddFormField('files', ExtractFileName(PdfFile), '', GetMIMETypeFromFile(PdfFile), PdfFile); if XmlFile<>'' then FormData.AddFile('files', XmlFile, GetMIMETypeFromFile(XmlFile)); // FormData.AddFormField('files', ExtractFileName(XmlFile), '', GetMIMETypeFromFile(XmlFile), XmlFile); Response:= HTTP.Post('https://<provider>/Documents/Outbox', FormData); // ... except on E: EIdHTTPProtocolException do begin OutputDebugStr(pchar('HTTP-Fehler: '+E.ErrorMessage+#13#10+'Antwort des Servers: '+E.Message)); end; on E: Exception do OutputDebugStr(pchar('Allgemeiner Fehler: '+E.Message)); end; FormData.Free; HTTP.Free; end;
Code:
Hat evtl. jemand sowas schon mal erfolgreich versucht und einen Tip für mich?
procedure SendFilesREST(const PdfFile, XmlFile: string);
var RESTClient:= TRESTClient; RESTRequest:= TRESTRequest; RESTResponse:= TRESTResponse; MultipartData: TMultipartFormData; begin RESTClient:= TRESTClient.Create(nil); RESTRequest:= TRESTRequest.Create(nil); RESTResponse:= TRESTResponse.Create(nil); RESTRequest.Client:= RESTClient; RESTRequest.Response:= RESTResponse; RESTRequest.Method:= rmPost; RESTRequest.ConnectTimeout:= -1; RESTRequest.ReadTimeout:= -1 RESTRequest.Params.AddHeader('Accept', 'application/json'); RESTRequest.Params.AddHeader('X-Api-Key', 'My-API-Key'); RESTRequest.Params.AddHeader('Content-Type', 'multipart/form-data'); MultipartData:= TMultipartFormData.Create; try FRESTClient.BaseURL:= 'https://<provider>/Documents/Outbox'; FRESTRequest.Method:= rmPOST; if PdfFile<>'' then MultipartData.AddFile('files', PdfFile, GetMIMETypeFromFile(PdfFile)); if XmlFile<>'' then MultipartData.AddFile('files', XmlFile, GetMIMETypeFromFile(XmlFile)); FRESTRequest.AddBody(MultipartData); FRESTRequest.Execute; if FRESTResponse.Status.Success then begin // ... end; finally RESTClient.Free; RESTRequest.Free; RESTResponse.Free; MultipartData.Free; end; end; |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Schon mit RestDebugger versucht, der kann nach korrekter Verbindung direkt die voreingestellten Komponenten erzeugen.
![]() |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Schau Dir den Code mit dem Restclient nochmal genauer an.
Du erstellt zwar in der Funktion einen lokalen RESTClient und einen RESTRequest, referenziert dann aber globale FRESTClient und FRESTRequest. Das kann nicht klappen. |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Falls das, was gubbe geschrieben hat, nur ein Tippfehler hier im Forum war, hab ich mal in meinem Code gewühlt, weil ich so grob etwas ähnliches schon gebastelt hatte. Bei mir musste ein JSON gefolgt von einem PDF gesendet werden. Ich habe das damals so gelöst (erzeugen der ganzen REST-Objekte passierte außerhalb der hier gezeigten Funktion, hier wird nur der RESTRequest zusammengesetzt, aber sollte für den Kern reichen):
Delphi-Quellcode:
So hat es problemlos funktioniert. Wichtig ist dabei sicherlich mein Kommentar. Ich muss allerdings auch sagen, dass ich damals bei meinen Versuchen nicht über "TMultipartFormData" gestolpert bin. Lese ich hier gerade zum ersten Mal, man lernt ja nie aus. ;-) Vielleicht wäre das auch in meinem Fall damit irgendwie besser/eleganter gegangen, aber das oben ist jedenfalls eine (für mich) funktionierende Lösung.
function SendInvoiceAndPDFToCRM (aRestReq: TRESTRequest; aInvoice: TecsDataCustomerInvoice; aExcelWriter: TinvoiceExcelWriter; var oErrorMsg: string): boolean;
begin Result:=true; aRestReq.Body.Add(TJson.ObjectToJsonObject(aInvoice),ooREST); if (aExcelWriter.PDFName<>'') and FileExists(aExcelWriter.PDFName) then // wir müssen die Datei hier mit Params.AddItem hinzufügen, sonst können wir sie nicht als pkFILE // kennzeichnen und wenn wir das nicht machen, dann rafft Delphi die Sache mit den Multiparts nicht // (Bug in REST.Client -> TCustomRESTRequest.ContentType seit Delphi 11.2) aRestReq.Params.AddItem('invoiceFile',TFileStream.Create(aExcelWriter.PDFName,fmOpenRead),pkFILE,[poDoNotEncode],ctAPPLICATION_PDF,ooREST); aRestReq.Method:=rmPOST; aRestReq.Resource:='/updateInvoice'; try aRestReq.Execute; except on e: Exception do begin oErrorMsg:='Fehler beim Senden einer neuen Rechnung. '+e.Message; Result:=false; end; end; [...] end; |
AW: REST Umsetzung eines CURL Befehls nach Delphi
@Rollo62
Ja, ich kenne den REST-Debugger. Aber leider finde ich keine Infos dazu, wie ich die Dateien dort anhängen kann. @gubbe Ja, hast Recht - hatte ich übersehen beim Erstellen des Codes hier in Forum. Normalerweise sind das Elemente der Klasse. @Bbommel Danke für den Code. Leider bekomme ich aber noch immer ein http/400. :( Da "AddItem" nicht mehrere gleichlautenden Parameter zulässt, habe ich das zum Test "MultipartData" entfernt und wie folgt geändert
Code:
Selbes für die 2te Datei.
if PdfFile<>'' then begin
RESTParam:= RESTRequest.Params.AddItem; RESTParam.Name:= 'files'; RESTParam.SetStream(TFileStream.Create(PdfFile, fmOpenRead), ooREST); RESTParam.Kind:= pkFILE; RESTParam.Options:= [poDoNotEncode]; RESTParam.ContentType:= GetMIMETypeFromFile(PdfFile); end; Der Anbieter der API schreibt das die Dateien als "files" und als "array" zu übertragen sind - ist das damit gegeben? Ober übersehe ich eine Methode um mehrere Dateien mit identischem "Schlüssel" zu übertragen? |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Zitat:
|
AW: REST Umsetzung eines CURL Befehls nach Delphi
Ich habe meinen Code mal durchsucht nach einem RestRequest mit Dateiübertragung.
Dabei habe ich auch einfach "Request.addfile" verwendet, aber das war nur eine Datei. Das Problem ist vielleicht wirklich der Dateiname "files". Hast Du es schon einmal mit Varianten probiert wie files[0] und files[1]? Ansonsten, wenn es mit Curl funktioniert, würde ich mir mal die HTTP-Kommunikation in einem "Debugging Proxy" anschauen. z.B. mit mitmproxy, fiddler oder Charles. Dann kannst Du vergleichen und gezielt die Unterschiede ausbügeln. |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Wenn Du bei Curl noch "--trace -" bzw. "--trace-ascii -" dranhängst, siehst Du auch, wie es versendet wird. Vielleicht gibt das schon einen Hinweis darauf, wie es aussehen sollte.
Probiere auch mal "files[]" als Dateiname. |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Liste der Anhänge anzeigen (Anzahl: 2)
Also wenn ich unter Postman den Request simuliere (funktioniert) und anschließend eine "Mock collection" starte sehe ich, dass mein von Delphi an die Mock Adresse gesendeter Request für den Content-Typ keine "boundary" enthält.
Anhang 57262 Anhang 57263 Hätte nicht gedacht dass sdas so kompliziert wird. Werde wohl versuchen vom Anbieter ein Beispiel zu bekommen. BTW: Es müssen keine 2 Dateien versendet werden - eine reicht auch. Dennoch immer nur 404... |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Ich empfehle auch die Bordmittel (RESTRequest.AddFile) zu verwenden. Dabei werden sowohl der ContentType als auch die Boundary entsprechend gesetzt. Eine dementsprechend korrigierte Version deines Codes könnte dann so aussehen:
Delphi-Quellcode:
procedure SendFilesREST(const PdfFile, XmlFile: string);
var RESTClient: TRESTClient; RESTRequest: TRESTRequest; RESTResponse: TRESTResponse; begin RESTRequest:= TRESTRequest.Create(nil); try RESTClient:= TRESTClient.Create(RESTRequest); RESTResponse:= TRESTResponse.Create(RESTRequest); RESTRequest.Client:= RESTClient; RESTRequest.Response:= RESTResponse; RESTRequest.Method:= rmPost; RESTRequest.ConnectTimeout:= -1; RESTRequest.ReadTimeout:= -1; RESTRequest.Accept := TRESTContentType.ctAPPLICATION_JSON; RESTRequest.AddAuthParameter('X-Api-Key', 'My-API-Key', TRESTRequestParameterKind.pkHTTPHEADER); RESTClient.BaseURL:= 'https://<provider>/Documents/Outbox'; RESTRequest.Method:= rmPOST; if PdfFile<>'' then RESTRequest.AddFile('files', PdfFile, TRESTContentType.ctAPPLICATION_PDF); if XmlFile<>'' then RESTRequest.AddFile('files', XmlFile, TRESTContentType.ctTEXT_XML); RESTRequest.Execute; if RESTResponse.Status.Success then begin // ... end; finally RESTRequest.Free; end; end; |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Ich fürchte trotzdem, dass zwei Parameter mit gleichem Namen (files) so nicht funktionieren werden, weil dann die zweite Datei die erste überschreibt. Wenn man sich TRESTRequestParameterList.AddItem anschaut, wird erst nach einem Parameter mit gleichen Namen gesucht und dann ggf. ersetzt.
Aber das scheint ja noch gar nicht das Problem zu sein, wenn man auch nur eine Datei übertragen könnte. Dass Delphi im Content-Type das Boundary nicht vorher definiert, sehe ich auch. Gibt denn der Server ausser dem HTTP-Code 400 noch etwas anderes zurück, das bestätigen könnte, dass es an der Boundary-Definiton liegt? |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Ah, jetzt sehe ich das Problem:
Setze den Content-Type Header nicht selbst, dann ist auch das Boundary enthalten. Ausserdem würde ich auch den "Accept-Header" so setzen:
Delphi-Quellcode:
Und nicht wie vorher mit RESTRequest.Params.AddHeader('Accept', 'application/json');
RestRequest.Accept := 'application/json';
Sonst wird der Slash evtl. falsch kodiert und kommt falsch an. Edit: Uwe hat das ja letztlich schon alles so (und besser) geschrieben in seinem korrigierten Code. Wirkt auch gleich aufgeräumter, wenn man die Konstanten von TRESTContentType verwendet. |
AW: REST Umsetzung eines CURL Befehls nach Delphi
@Uwe, @gubbe
Vielen Dank für die Hinweise. Der von Uwe geänderte Code funktioniert für eine Datei. Das Problem ist jedoch, dass AddFile intern AddItem verwendet, welches auf den Namen prüft und eine doppelte Verwendung nicht zulässt. Mein Ansatz über:
Code:
schlägt jedoch auch fehl, da in innerhalb von RESTRequest.Execute nochmals die Parameterliste neu aufgebaut und doppelten Einträge aussortiert werden - keine Ahnung wieso man das macht und nicht dem Programmierer überlässt.
RESTParam:= FRESTRequest.Params.AddItem;
RESTParam.Name:= 'files'; RESTParam.Value:= ExtractFileName(PdfFile); RESTParam.SetStream(TFileStream.Create(PdfFile, fmOpenRead), TRESTObjectOwnership.ooREST); RESTParam.Kind:= TRESTRequestParameterKind.pkFILE; RESTParam.Options:= [TRESTRequestParameterOption.poDoNotEncode]; RESTParam.ContentType:= GetMIMETypeFromFile(PdfFile); Mein letzter Versuch war noch über MultipartData.AddFile zu gehen, aber entweder bin ich zu doof, irgendwas stimmt mit folgender Zuweisung nicht.
Code:
Wird auch ablehnt. Wie auch immer...
FRESTRequest.AddBody(MultipartData.Stream, MultipartData.MimeTypeHeader, TRESTObjectOwnership.ooREST);
Ich verwende nun folgenden Code:
Code:
Zumindest zeigt Postman eine plausible Content-Length an und wie API akzeptiert auch den Request.
procedure SendFilesIndy2(const PdfFile, XmlFile: string);
var HTTP: TIdHTTP; FormData: TIdMultiPartFormDataStream; url, Response: string; begin HTTP:= TIdHTTP.Create(nil); FormData:= TIdMultiPartFormDataStream.Create; try HTTP.Request.Accept:= 'application/json'; HTTP.Request.ContentType:= 'multipart/form-data'; HTTP.Request.CustomHeaders.AddValue('X-Api-Key', MY_API_KEY); AttachSSLHandler(HTTP, [sslvTLSv1_2]); if PdfFile<>'' then FormData.AddFile('files', PdfFile, GetMIMETypeFromFile(PdfFile)); if XmlFile<>'' then FormData.AddFile('files', XmlFile, GetMIMETypeFromFile(XmlFile)); url:= 'MyURL'; Response:= HTTP.Post(url, FormData); // ... except // on E: EIdHTTPProtocolException do end; FormData.Free; HTTP.Free; end; Muss nur noch mit dem Anbieter klären, ob die Dateien auch korrekt empfangen wurden - im Moment gehe ich aber davon aus. Jetzt stehe ich aber von einem Gewissensproblem: Bleibe ich bei den übrigen API-Aufrufen beider Kombi aus RESTClient/Request/Response oder stelle ich komplett auf Indy um? |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Ich würde mir echt überlegen, ob es eine gute Idee ist, auf Indy zu gehen. Du müsstest die OpenSSL-Bibliotheken zusammen mit deinem Produkt ausliefern und selbst auf die Aktualisierung achten und zumindest in dem standardmäßig mitgelieferten Indy kann man aktuelle OpenSSL-Versionen nicht mit einbauen. Mir wäre das zu kritisch.
Du könntest alternativ die REST-Unit aus der Delphi-RTL in dein Programmverzeichnis legen und dort eine angepasste Version erstellen, welche es zulässt, dass es mehrere Parts mit der gleichen Bezeichnung gibt. Ist auch nicht richtig schön, aber in einen saueren Apfel muss man wohl beißen. Ob es "richtig", also standardkonform ist, was Delphi macht oder was der Anbieter deiner API macht, weiß ich gar nicht. Da müsste man sich durch irgendwelche RFC-Dokumente wühlen. Wenn aber mehrere Parts mit gleichem Namen zulässig sind, sollte man mal einen Bugreport dafür aufmachen. Das nur mal als abschließende Gedanken dazu... |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Schau Dir mal die procedure TCustomRESTRequest.DoPrepareRequestBody an.
Die ist virtuell und damit könntest Du sie in einer abgeleiteten Klasse überschreiben. Ein Ansatz könnte sein, die Dateien vorher unter verschiedenen Namen hinzuzufügen und hier einmal das Array der TRESTRequestParameter durchzugehen und die Namen nochmal zu ändern, um dann mit inherited die Funktion der Basis-Klasse aufzurufen. |
AW: REST Umsetzung eines CURL Befehls nach Delphi
@Bbommel
Da hast du Recht. Wäre aber kein Problem da alles eh nur auf 1 Installation und nur inhouse läuft. @gubbe Das war eine super Idee! Ich setze die Parameter nun wie folgt:
Code:
...und überschreibe die Funktion wie folgt...
if PdfFile<>'' then begin
RESTParam:= FRESTRequest.Params.AddItem; RESTParam.Name:= 'files[0]'; RESTParam.Value:= ExtractFileName(PdfFile); RESTParam.SetStream(TFileStream.Create(PdfFile, fmOpenRead), TRESTObjectOwnership.ooREST); RESTParam.Kind:= TRESTRequestParameterKind.pkFILE; RESTParam.Options:= [TRESTRequestParameterOption.poDoNotEncode]; RESTParam.ContentType:= GetMIMETypeFromFile(PdfFile); end; if XmlFile<>'' then begin RESTParam:= FRESTRequest.Params.AddItem; RESTParam.Name:= 'files[1]'; RESTParam.Value:= ExtractFileName(XmlFile); RESTParam.SetStream(TFileStream.Create(XmlFile, fmOpenRead), TRESTObjectOwnership.ooREST); RESTParam.Kind:= TRESTRequestParameterKind.pkFILE; RESTParam.Options:= [TRESTRequestParameterOption.poDoNotEncode]; RESTParam.ContentType:= GetMIMETypeFromFile(XmlFile); end;
Code:
Die API akzeptiert und Postman zeigt als Content-Length ca. die Summe beider Dateien!
procedure TMyAPi.TMyRESTRequest.DoPrepareRequestBody(
AParamList: TRESTRequestParameterArray; AContentType: TRESTContentType; var ABodyStream: TStream; var ABodyStreamOwner: Boolean); var P: TRESTRequestParameter; begin for P in AParamList do if StartsText('files[', P.Name) then P.Name:= 'files'; inherited; end; Bleibt zu hoffen dass der Anbieter das auch so sieht. |
AW: REST Umsetzung eines CURL Befehls nach Delphi
Zitat:
Es gibt übrigens einen Report für dieses Problem ( ![]() |
Alle Zeitangaben in WEZ +1. Es ist jetzt 11:16 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz