I’ve hit a client glitch using JsonHttpClient.Post<IReturnVoid>()
to send a multi-part file upload request.
I have code that worked using ServiceStack.Client v5.9.0 but no longer works using v5.10.4.
The JsonHttpClient.Post<IReturnVoid>()
method fails to return once the response is received. It just hangs, and the client process needs to be terminated.
The endpoint responding to the request is very non-standard and could not be used with the stock JsonHttpClient.PostFileWithRequest()
method. So I had to write my own FileUploader class to consume this weird API, and I based it on the ServiceStack client code, and then started hacking. It’s been working fine for a few years, on a few of this system’s esoteric file-upload APIs, but I discovered that this one API has caused clients using ServiceStack 5.10.4 to break since it was released in January.
This particular API responds with a 201 (No Content)
and a Location
header with a URL the client should follow, like a redirect.
The entire purpose of the FileUploader
class is to issue the POST multi-part file and use a ResultsFilterResponse
to save any received Location
header value to a string property.
Here is an example request sent by this code:
POST https://scc.aqsamples.com.au/api/v2/observationimports/dryrun?fileType=SIMPLE_CSV&timeZoneOffset=-07%3a00&linkFieldVisitsForNewObservations=True HTTP/1.1
Authorization: token 01234567890123456789
User-Agent: ServiceStack .NET Client 5.104/Aquarius.Client 20.3.1.0/LabFileImporter 1.0.0.0
Accept: application/json
Content-Type: multipart/form-data; boundary="e87a7ba1-c7d6-41a9-90fe-f00681a354f7"
Host: scc.aqsamples.com.au
Content-Length: 8396
Expect: 100-continue
Accept-Encoding: gzip, deflate
--e87a7ba1-c7d6-41a9-90fe-f00681a354f7
Content-Disposition: form-data; name=file; filename="LabFileImporter (v1.0.0.0) Uploads.csv"
Content-Type: text/csv
Observation ID,Location ID,Observed Property ID,Observed DateTime,Analyzed DateTime,Depth,Depth Unit,Data Classification,Result Value,Result Unit,Result Status,Result Grade,Medium,Activity ID,Activity Name,Collection Method,"Field: Device ID","Field: Device Type","Field: Comment","Lab: Specimen Name","Lab: Analysis Method","Lab: Detection Condition","Lab: Limit Type","Lab: MDL","Lab: MRL","Lab: Quality Flag","Lab: Received DateTime","Lab: Prepared DateTime","Lab: Sample Fraction","Lab: From Laboratory","Lab: Sample ID","Lab: Dilution Factor","Lab: Comment","QC: Type","QC: Source Sample ID"
,05MOC,Temperature,2020-04-22T09:00:00.000-07:00,2020-04-22T09:00:00.000-07:00,,,FIELD_RESULT,24.8,DegC,Preliminary,,Saline Water,,,,,,,,,,,,,,,,,,,,,,
< 20 rows removed >
,04MIC,Chloroa Phenophytin - ratio,2020-04-22T09:20:00.000-07:00,2020-04-22T09:20:00.000-07:00,,,LAB,1,ratio,Requested,,Saline Water,,22Apr2020-04MIC-09:20-REPLICATE,,,,,Properties,CM37;Chlorophyll a;Unity Water,,,,,,,,,15,,,,REPLICATE,
--e87a7ba1-c7d6-41a9-90fe-f00681a354f7--
And here is the HTTP response, received within a second.
HTTP/1.1 201 Created
Date: Wed, 31 Mar 2021 22:07:23 GMT
Connection: keep-alive
Server: nginx/1.15.9
Cache-Control: no-cache, private, max-age=0, no-store
Location: https://scc.aqsamples.com.au/api/v2/observationimports/393b4bfc-f740-430f-8a39-f6756e01cb3b/status
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=316224000
Content-Security-Policy: default-src https:; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src https: data: blob:
Content-Length: 0
But the JsonHttpClient.Post() method does not return, nor does the ResultsFilterResponse
callback get invoked.
When paused within Visual Studio, the stack trace is as follows. Any ideas on what to try next?
mscorlib.dll!System.Threading.Monitor.Wait(object obj, int millisecondsTimeout, bool exitContext)
mscorlib.dll!System.Threading.Monitor.Wait(object obj, int millisecondsTimeout)
mscorlib.dll!System.Threading.ManualResetEventSlim.Wait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)
mscorlib.dll!System.Threading.Tasks.Task.SpinThenBlockingWait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)
mscorlib.dll!System.Threading.Tasks.Task.InternalWait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)
mscorlib.dll!System.Threading.Tasks.Task<ServiceStack.IReturnVoid>.GetResultCore(bool waitCompletionNotification)
mscorlib.dll!System.Threading.Tasks.Task<System.__Canon>.Result.get()
[Waiting on Async Operation, double-click or press enter to view Async Call Stacks]
ServiceStack.HttpClient!ServiceStack.InternalExtensions.GetSyncResponse<System.__Canon>(System.Threading.Tasks.Task<System.__Canon> task) Line 1169
at C:\BuildAgent\work\3481147c480f4a2f\src\ServiceStack.HttpClient\JsonHttpClient.cs(1169)
ServiceStack.HttpClient!ServiceStack.JsonHttpClient.Post<ServiceStack.IReturnVoid>(string relativeOrAbsoluteUrl, object request) Line 919
at C:\BuildAgent\work\3481147c480f4a2f\src\ServiceStack.HttpClient\JsonHttpClient.cs(919)
Aquarius.Client!Aquarius.Samples.Client.FileUploader.PostFileWithRequest<System.__Canon>(string relativeOrAbsoluteUri, System.IO.Stream contentToUpload, string uploadedFileName, System.__Canon requestDto, System.Net.Http.HttpContent extraContent, string extraContentName)
Aquarius.Client!Aquarius.Samples.Client.SamplesClient.PostFileWithRequest.AnonymousMethod__0()
Aquarius.Client!Aquarius.Samples.Client.SamplesClient.InvokeWebServiceMethod.AnonymousMethod__0()
Aquarius.Client!Aquarius.Samples.Client.SamplesClient.InvokeWebServiceMethod<int>(System.Func<int> webServiceMethod, System.Func<ServiceStack.Text.JsConfigScope> scopeMethod)
Aquarius.Client!Aquarius.Samples.Client.SamplesClient.InvokeWebServiceMethod(System.Action webServiceMethod, System.Func<ServiceStack.Text.JsConfigScope> scopeMethod)
Aquarius.Client!Aquarius.Samples.Client.SamplesClient.PostFileWithRequest<Aquarius.Samples.Client.ServiceModel.PostObservationsDryRunV2>(System.IO.Stream contentToUpload, string uploadedFileName, Aquarius.Samples.Client.ServiceModel.PostObservationsDryRunV2 requestDto, System.Net.Http.HttpContent extraContent, string extraContentName)
LabFileImporter.exe!LabFileImporter.ImportClient.PostImportDryRunForStatusUrl(string filename, byte[] contentBytes) Line 58
at C:\git\Examples\Samples\DotNetSdk\LabFileImporter\ImportClient.cs(58)
LabFileImporter.exe!LabFileImporter.Importer.ImportObservationsToSamples(System.Collections.Generic.List<LabFileImporter.ObservationV2> observations) Line 126
at C:\git\Examples\Samples\DotNetSdk\LabFileImporter\Importer.cs(126)
LabFileImporter.exe!LabFileImporter.Importer.Import() Line 40
at C:\git\Examples\Samples\DotNetSdk\LabFileImporter\Importer.cs(40)
LabFileImporter.exe!LabFileImporter.MainForm.ImportFiles(string[] paths) Line 260
at C:\git\Examples\Samples\DotNetSdk\LabFileImporter\MainForm.cs(260)
LabFileImporter.exe!LabFileImporter.MainForm.TryImportFiles(string[] paths) Line 228
at C:\git\Examples\Samples\DotNetSdk\LabFileImporter\MainForm.cs(228)
LabFileImporter.exe!LabFileImporter.MainForm.importButton_MouseClick(object sender, System.Windows.Forms.MouseEventArgs e) Line 206
at C:\git\Examples\Samples\DotNetSdk\LabFileImporter\MainForm.cs(206)