Cannot upload file to ktor server using ktor client - file-upload

I have setup server according to docs and try to upload file using code from this question:
val parts: List<PartData> = formData {
append(
"image",
InputProvider { ins.asInput() },
Headers.build {
this[HttpHeaders.ContentType] = "image/png"
this[HttpHeaders.ContentDisposition] = "filename=$name"
}
)
}
return HttpClient(Apache) {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}.submitFormWithBinaryData(formData = parts) {
url("$baseUrl/images")
}
If I use it as is (without request Content-Type), then server fails: "Content-Type header is required for multipart processing".
If I try to add header, client fails: "Header Content-Type is controlled by the engine and cannot be set explicitly".
Then it's actually something strange happening.
According to client logs, it's sending content type:
REQUEST: http://localhost:8090/images
METHOD: HttpMethod(value=POST)
COMMON HEADERS
-> Accept: */*
-> Accept-Charset: UTF-8
CONTENT HEADERS
BODY Content-Type: multipart/form-data; boundary=-675255df42a752ee167beaab-5799548c6088f411-a7e8dc449d68ab028c44d80-42b
BODY START
[request body omitted]
...
But on server side headers are completly different:
Accept-Charset [UTF-8]
Accept [*/*]
User-Agent [Ktor client]
Transfer-Encoding [chunked]
Host [localhost:8090]
Connection [Keep-Alive]
On other hand I can successfully upload file using okhttp (and headers actually matches):
val logging = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String ) {
println(message)
}
})
logging.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
val file = File("image.png")
val part: MultipartBody.Part = MultipartBody.Part.Companion.createFormData(
"image",
"image.png",
file.asRequestBody("image/png".toMediaTypeOrNull())
)
val request = Request.Builder()
.url("http://localhost:8090/images")
.post(MultipartBody.Builder().addPart(part).build())
.build()
val res = client.newCall(request).execute()
res.body
Is it bug in ktor client or I missing something?
edit:
Both client and server versions is 1.4.1.
Corresponding gradle dependencies parts:
implementation("io.ktor:ktor-server-core:${ktor_version}")
implementation("io.ktor:ktor-server-netty:${ktor_version}")
implementation("io.ktor:ktor-jackson:$ktor_version")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.8")
...
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")
implementation("io.ktor:ktor-client-jackson:$ktor_version")
implementation("io.ktor:ktor-client-logging:$ktor_version")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.2")
route:
object ImagesRouter {
fun Routing.images(imagesModule: ImagesModule) {
route("images") {
get("/{id}") {
// ...
}
post {
val multipart = call.receiveMultipart() // fails here
// ...
}
}
}
}

Related

Example of URL builder in Ktor

I'm using Ktor client to make calls to an API and I didn't find any examples of how to construct a URL with query parameters.
I wanted something like this:
protocol = HTTPS,
host = api.server.com,
path = get/items,
queryParams = List(
Pair("since", "2020-07-17"),
)
I can't find any examples of how to use URL builder for this.
If you want to specify each of this element (protocol, host, path and params) separately you can use a HttpClient.request method to construct your url. Inside this method you have access to HttpRequestBuilder and then you can configure url with usage of UrlBuilder
client.request<Response> {
url {
protocol = URLProtocol.HTTPS
host = "api.server.com"
path("get", "items")
parameters.append("since", "2020-07-17")
}
}
Response type is your response, you can specify there whatever you need
It would also be helpful if someone wants to add a base URL to all their requests :
HttpClient(Android) {
expectSuccess = false
//config Client Serialization
install(JsonFeature) {
serializer = KotlinxSerializer(json)
}
//config client logging
install(Logging) {
level = LogLevel.BODY
}
//Config timeout
install(HttpTimeout) {
requestTimeoutMillis = 30 * 1000L
connectTimeoutMillis = 10 * 1000L
}
//Config Base Url
defaultRequest {
url {
protocol =URLProtocol.HTTPS
host = baseUrl
}
}
}
val json = kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
}

Getting error while uploading image using multipart request in Alamofire

xcode 10.3
swift 4.2
iPhone app
I'm hitting a post type API to upload image along with some string data from a form. I'm getting response result as success but there is an error parameters which indicates: CredStore - performQuery - Error copying matching creds. Error=-25300
I have tried doing some research and found that I've problem with URLCredentialStorage but I don't have any Credentials in API.
I looked at CredStore Perform Query error, I'm having very similar issue but can't find my solution there.
{
let url = route.asURL(constants: constants)
let parameters = route.asParameters(constants: constants)
let headers: HTTPHeaders = [
"authorization-name": APISession.token,
"Content-Type": "multipart/form-data"
]
Log(request: URLRequest(url: url))
Alamofire.upload(multipartFormData: { (multipartFormData) in
for (key, value) in parameters {
if let strValue = value as? String, let valueData = strValue.data(using: .utf8) {
multipartFormData.append(valueData, withName: key)
}
if let imgValue = value as? UIImage, let imgData = imgValue.jpegData(compressionQuality: 0.5) {
multipartFormData.append(imgData, withName: key, mimeType: "image/jpg")
}
}
}, usingThreshold: UInt64.init(), to: url, method: .post, headers: headers) { (result) in
print(result)
switch result{
case .success(let upload, _, _):
upload.uploadProgress(closure: { (progress) in
print("Preogress is: ", progress.fractionCompleted)
})
upload.responseJSON { response in
print("Succesfully uploaded")
switch response.result {
case .success(let value):
print(value)
case .failure(let error):
print(error)
}
}
case .failure(let error):
print("Error in upload: \(error.localizedDescription)")
}
}
}
I'm getting response like this,
success(request: 2019-08-12 13:07:04.224913-0400 project name[65508:460860] CredStore - performQuery - Error copying matching creds. Error=-25300, query={
class = inet;
"m_Limit" = "m_LimitAll";
ptcl = htps;
"r_Attributes" = 1;
sdmn = "url";
srvr = "url";
sync = syna;
}
so I'm confused why I'm getting success response with error parameters in that.
Not sure but I think authorization-name key present in headers is incorrect. It should be Authorization.

Send application/x-www-form-urlencoded in Ktor

I can't figure out how to send a application/x-www-form-urlencoded POST request in Ktor. I see some submitForm helpers in Ktor's documentation but they don't send the request as expected.
What I want is to replicate this curl line behavior:
curl -d "param1=lorem&param2=ipsum" \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
https://webservice/endpoint
My dependency is on io.ktor:ktor-client-cio:1.0.0.
After several tries I managed to send the request with the following code:
val url = "https://webservice/endpoint"
val client = HttpClient()
return client.post(url) {
body = FormDataContent(Parameters.build {
append("param1", "lorem")
append("param2", "ipsum")
})
}
val response: HttpResponse = client.submitForm(
url = "http://localhost:8080/get",
formParameters = Parameters.build {
append("first_name", "Jet")
append("last_name", "Brains")
},
encodeInQuery = true
)
https://ktor.io/docs/request.html#form_parameters
Looking for information I found the way to do it
suspend inline fun <reified T> post(path: String, requestBody: FormDataContent): T {
return apiClient.post() {
url {
encodedPath = path
contentType(ContentType.Application.FormUrlEncoded)
}
setBody(requestBody)
}.body()
}

Alamofire.download by post with parameters not working

Hello I am doing download file by post with parameters. But server can't receive post parameters.
But if i do same thing with get with url parameters. Everything works fine.
Almofire.request also works fine by post with parameters. But only Almofire.download by post with parameter does not work.
Why Alamofire.download does not send paramters by post method ??
var sourceStringURL : String = "\(tmp_url)download"
let destination: DownloadRequest.DownloadFileDestination =
{
_, _ in
let fileURL = URL(fileURLWithPath: destPath)
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
Alamofire.download(sourceStringURL, method: .post, parameters: ["id": idStr, "var": varStr], encoding: JSONEncoding.default, headers: nil, to: destination)
.downloadProgress
{
progress in
var tmpPercent : Int = Int(progress.fractionCompleted*100 / 1.0)
}
.response
{
response in
if let error = response.error
{
print(error)
}
else
{
//success
}
}
Server receives post request correctly with Retrofit library in Android.
I just found that if i change JSONEncoding.default to URLEncoding.default.
It works fine.

Swift - 8tracks.com API POST json data fails (422) everytime

I'm trying to talk with 8tracks open API in Swift iOS app. I need to make POST authorization request to http://8tracks.com/sessions.jsonwith AFNetworking but everytime I get 422 Unprocessable Entity error..
I tried this endpoint on the web and it works fine. Here is code that I'm using (subclassing AFHTTPSessionManager):
init() {
super.init()
self.responseSerializer = AFJSONResponseSerializer()
self.requestSerializer = AFJSONRequestSerializer()
self.requestSerializer.setValue(API_KEY, forHTTPHeaderField: "X-Api-Key")
self.requestSerializer.setValue("3", forHTTPHeaderField: "X-Api-Version")
}
func login(username: String, password: String, success: (NSURLSessionDataTask!, AnyObject!) -> Void, failure: ((NSURLSessionDataTask!, NSError!) -> Void)?) {
let credentials = ["username": username, "password": password] as Dictionary
self.POST(
API_URL.stringByAppendingString("/sessions.json"),
parameters: credentials,
success: success,
failure: failure
)
}
Error looks as follows:
{ URL: http://8tracks.com/sessions.json } { status code: 422, headers {
"Accept-Ranges" = bytes;
"Access-Control-Allow-Origin" = "*";
Age = 0;
"Cache-Control" = "max-age=0, private, must-revalidate";
Connection = "keep-alive";
"Content-Length" = 125;
"Content-Type" = "application/json; charset=utf-8";
Date = "Wed, 25 Jun 2014 19:29:12 GMT";
Server = "nginx/1.4.3";
Status = "422 Unprocessable Entity";
Via = "1.1 varnish";
"X-Action" = "sessions/create";
"X-Backend" = rails;
"X-Cache" = MISS;
"X-Data-Request" = 1;
"X-Request-Id" = 3040c8bf79936b27075731f634bfd534;
"X-Requests-Left" = 99;
"X-Runtime" = "0.257240";
"X-UA-Compatible" = "IE=Edge,chrome=1";
} }, NSLocalizedDescription=Request failed: client error (422),
NSErrorFailingURLKey=http://8tracks.com/sessions.json}
It might have something to do with subclass AFHTTPSessionManager without a baseURL. I've tested the following code and it works.
let path = "/sessions.json"
let params = ["login": login, "password": password, "api_version": "3"]
let success = {(task: NSURLSessionDataTask!, response: AnyObject!) -> Void in
println(response)
}
let failure = {(task: NSURLSessionDataTask!, error: NSError!) -> Void in
println(error)
}
var client = AFHTTPSessionManager(baseURL: NSURL(string: "https://8tracks.com"))
client.POST(path, parameters: params, success: success, failure: failure)