StackOverflow 👋
I'm putting an object to S3 directly from the browser via a signedUrl.
The code I'm using looks roughly like this:
const formData = new FormData()
const file = await selectorToInput.files[0]
formData.append('file', file)
await fetch(uploadUrl, {
method: 'PUT',
body: formData,
mode: 'cors',
headers: {
'Content-Type': 'text/yaml',
}
}).then(r => r.ok)
The upload is successful, however, when retrieving the objects after they've been uploaded they're all prefixed with characters like this:
-----------------------------3536405376111676041452100156
'Content-Disposition: form-data; name="file"
<THE REST OF THE FILE CONTENTS>
They look like file headers of some kind, but can't figure out for the life of me where they're coming from. For more context, these files are all yaml and these additional characters are causing the parser I'm using to throw malformed yaml errors, so I don't feel like they're suppose to be there.
I've also tried this without using the .text() call on the File object and get the same result with different looking headers. Is this an issue/feature of Fetch?
I wish I could provide more info, but I've been searching for several hours now and haven't found an explanation. Any help is greatly appreciated.
Answering my own issue here. After more digging I found this issue on the amazon sdk. Turns out that wrapping the file in FormData was the issue. This goes contrary to all other documentation I found, so hopefully this helps someone else.
Related
I'm trying to figure out how to send an image to my API, and also verify a generated token that is in the header of the request.
So far this is where I'm at:
#app.post("/endreProfilbilde")
async def endreProfilbilde(request: Request,file: UploadFile = File(...)):
token=request.headers.get('token')
print(token)
print(file.filename)
I have another function that triggers the change listener and upload function, passing the parameter: bildeFila
function lastOpp(bildeFila) {
var myHeaders = new Headers();
let data = new FormData();
data.append('file',bildeFila)
myHeaders.append('token', 'SOMEDATAHERE');
myHeaders.append('Content-Type','image/*');
let myInit = {
method: 'POST',
headers: myHeaders,
cache: 'default',
body: data,
};
var myRequest = new Request('http://127.0.0.1:8000/endreProfilbilde', myInit);
fetch(myRequest)//more stuff here, but it's irrelevant for the Q
}
The Problem:
This will print the filename of the uploaded file, but the token isn't passed and is printed as None. I suspect this may be due to the content-type, or that I'm trying to force FastAPI to do something that is not meant to be doing.
As per the documentation:
Warning: When using FormData to submit POST requests using XMLHttpRequest or the Fetch_API with the
multipart/form-data Content-Type (e.g. when uploading Files and
Blobs to the server), do not explicitly set the Content-Type
header on the request. Doing so will prevent the browser from being
able to set the Content-Type header with the boundary expression
it will use to delimit form fields in the request body.
Hence, you should remove the Content-Type header from your code. The same applies to sending requests through Python Requests, as described here and here. Read more about the boundary in multipart/form-data.
Working examples on how to upload file(s) using FastAPI in the backend and Fetch API in the frontend can be found here, here, as well as here and here.
So I figured this one out thanks to a helpful lad in Python's Discord server.
function lastOpp(bildeFila) {
let data = new FormData();
data.append('file',bildeFila)
data.append('token','SOMETOKENINFO')
}
#app.post("/endreProfilbilde")
async def endreProfilbilde(token: str = Form(...),file: UploadFile = File(...)):
print(file.filename)
print(token)
Sending the string value as part of the formData rather than as a header lets me grab the parameter.
Hi I'm doing a coding challenge and I'm trying to fetch data from an html page but I keep getting the 400 error. I don't see where my syntax would be wrong. The Promise returns an empty string as PromiseResult. Why do I not get the data from https://adventofcode.com/2021/day/2/input?
fetch('https://adventofcode.com/2021/day/2/input', {
method: 'GET',
mode: 'no-cors',
credentials: 'omit',
headers: {
'Content-Type': 'text/plain'
}
})
.then((response) => {
response.text();
})
.then((html) => {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
})
.catch((err) => { console.log(err) })
After visting https://adventofcode.com/2021/day/2/input I get information that
Puzzle inputs differ by user. Please log in to get your puzzle input.
Please check if you are providing your login data correctly.
mode: 'no-cors',
This says that you are not going to do anything that requires permission to be granted using CORS. Anything that does require CORS will be quietly ignored.
You need CORS permission to read data across origins. Since you have said you aren't going to use CORS, you don't have permission, so you don't get any data.
This is why you get an empty string.
credentials: 'omit',
The input endpoint requires your credentials so that it can give you your input, which is unique to each user. Since told the browser not to send any, the end point doesn't understand the request.
As an aside:
headers: {
'Content-Type': 'text/plain'
}
This is just nonsense.
It claims that the request includes a body consisting of plain text.
You are making a GET request. There is no body on the request at all.
Advent of code expects you to manually download your input data. It doesn't expect your solution to fetch it from the AoC website.
JS solutions are generally run in Node.js (rather than a web browser) where they can use the fs module to read the local copy of the input file. (Tip: I find it handy to copy/paste the sample data into a sample file to test my results against the worked example on each day).
I'm trying to post the raw data of a picture, using Axios, after taking it with react-native-image-picker.
I successfully generated a blob using this piece of code:
const file = await fetch(response.uri);
const theBlob = await file.blob();
If I inspect the metadata of the blob it's all right: MIME type, size, and so on.
However, when I try to POST it using Axios, using:
axios({
method: "POST",
url: "https://my-api-endpoint-api.example.org",
data: theBlob,
});
what I receive on the API side is this strange JSON payload:
{"_data":{"lastModified":0,"name":"rn_image_picker_lib_temp_77cb727c-5056-4cb9-8de1-dc5e13c673ec.jpg","size":1635688,"offset":0,"type":"image/jpeg","blobId":"83367ee6-fa11-4ae1-a1df-bf1fdf1d1f57","__collector":{}}}
The same code is working fine on React, and I have the same behavior trying with a File object instead of a Blob one.
I see in other answers that I could use something else than Axios, like RNFetchBlob.fetch(), but since I'm using shared functions between the React website and the React Native app, I'd really prefer an approach that allows me to use Axios and Blobs.
Is there some way to work around it?
Updated answer
As pointed out by #T.J.Crowder in the comments, there is a cleaner approach that works around the issue of the React Native host environment, without touching anything else on the code.
It's enough to add this on the index.js file, before everything else:
Blob.prototype[Symbol.toStringTag] = 'Blob'
File.prototype[Symbol.toStringTag] = 'File'
I leave my original answer here under since it's a working alternative if one doesn't want to mess up with the prototype.
Original answer
The described behavior happens because the host environment of React Native does not handle the Blob type nicely: it will actually become just an object.
In fact, if you try to render toString.call(new Blob()) in a component, you'll see [object Blob] in a browser, but [object Object] in React Native.
The matter is that the default transformRequest implementation of Axios will use exactly this method (toString.call) to check if you're passing a Blob or some generic object to it. If it sees you're passing a generic object, it applies a JSON.stringify to it, producing the strange JSON you're seeing POSTed.
This happens exactly here: https://github.com/axios/axios/blob/e9965bfafc82d8b42765705061b9ebe2d5532493/lib/defaults.js#L61
Actually, what happens here is that utils.isBlob(data) at line 48 returns false, since it really just applies toString on the value and checks if it is [object Blob], which as described above is not the case.
The fastest workaround I see here, since you're sure you're passing a Blob, is just to override transformRequest with a function that just returns the data as it is, like this:
axios({
method: "POST",
url: "https://my-api-endpoint-api.example.org",
data: theBlob,
transformRequest: (d) => d,
});
This will just make the request work.
I actually had this problem recently (using Expo SDK 43 on iPhone). I remember using axios over fetch because I had problems with uploading blobs with fetch in the past. But I tried it here and it just worked.
The context in this use case is downloading a giphy from url and then putting it on s3 with a signed request. Worked on both web and phone.
const blob = await fetch(
giphyUrl
).then((res) => res.blob());
fetch(s3SignedPutRequest, { method: "PUT", body: blob });
You can send file into server using formData through axios API like below :
const file = await fetch(response.uri);
const theBlob = await file.blob();
var formData = new FormData();
theBlob.lastModifiedDate = new Date();
theBlob.name = "file_name";
formData.append("file", theBlob);
axios.post('https://my-api-endpoint-api.example.org', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
I receive a Request in my cloudflare worker and want to upload the data to Google cloud storage. My problem is that I can't extract the content type from the multipart/form-data data I receive in order to upload it with the correct content type to GCS.
When I read the request with await req.formData() I can get('file')from the formData and it returns the raw file data that I need for the GCS, but I can't seem to see anywhere the file content-type that I need (I can see it only when looking at the raw Request body).
Here is my (striped down) code :
event.respondWith((async () => {
const req = event.request
const formData = await req.formData()
const file = formData.get('file')
const filename = formData.get('filename')
const oauth = await getGoogleOAuth()
const gcsOptions = {
method: 'PUT',
headers: {
Authorization: oauth.token_type + ' ' + oauth.access_token,
'Content-Type': 'application/octet-stream' //this should by `'Content-Type': file.type`
},
body: file,
}
const gcsRes = await fetch(
`https://storage.googleapis.com/***-media/${filename}`,
gcsOptions,
)
if (gcsRes.status === 200) {
return new Response(JSON.stringify({filename}), gcsRes)
} else {
return new Response('Internal Server Error', {status: 500, statusText: 'Internal Server Error'})
}
})())
Reminder - the code is part of our cloudflare worker code.
It seems to me this should be straight forward, determining the type of file you extract from the multipart/form-data data.
Am I missing something?
Unfortunately, as of this writing, the Cloudflare Workers implementation of FormData is incomplete and does not permit extracting the Content-Type. In fact, it appears our implementation currently interprets all entries as text and return strings, which means binary content will be corrupted. This is a bug which will require care to fix since we don't want to break already-deployed scripts that might rely on the buggy behavior.
Thanks Kenton for your response.
What I ended up doing:
As the Cloudflare Workers don't support the multipart/form-data of Blob or any type other than String, I ended up using the raw bytes in the ArrayBuffer data type. After converting it to an Uint8Array I parsed it byte by byte to determine the file type and the start and end indexes of the file data. Once I found the start and end of the transferred file I was able create an array of the file data, add it to the request and send it to the GCS as I showed above.
I am able to upload to S3 using a file picker and regular XMLHttpRequest (which I was using to test the S3 setup), but cannot figure out how to do it successfully using the cordova file transfer plugin.
I believe it is either to do with the plugin not constructing the correct signable request, or not liking the local file uri given. I have tried playing with every single parameter from headers to uri types, but the docs aren't much help, and the plugin source is bolognese.
The string the request needs to sign match is like:
PUT
1391784394
x-amz-acl:public-read
/the-app/317fdf654f9e3299f238d97d39f10fb1
Any ideas, or possibly a working code example?
A bit late, but I just spent a couple of days struggling with this so in case anybody else is having problems, this is how managed to upload an image using the javascript version of the AWS SDK to create the presigned URL.
The key to solving the problem is in the StringToSign element of the XML SignatureDoesNotMatch error that comes back from Amazon. In my case it looked something like this:
<StringToSign>
PUT\n\nmultipart/form-data; boundary=+++++org.apache.cordova.formBoundary\n1481366396\n/bucketName/fileName.jpg
</StringToSign>
When you use the aws-sdk to generate a presigned URL for upload to S3, internally it will build a string based on various elements of the request you want to make, then create an SHA1 hash of it using your AWS secret. This hash is the signature that gets appended to the URL as a parameter, and what doesn't match when you get the SignatureDoesNotMatch error.
So you've created your presigned URL, and passed it to cordova-plugin-file-transfer to make your HTTP request to upload a file. When that request hits Amazon's server, the server will itself build a string based on the request headers etc, hash it and compare that hash to the signature on the URL. If the hashes don't match then it returns the dreaded...
The request signature we calculated does not match the signature you provided. Check your key and signing method.
The contents of the StringToSign element I mentioned above is the string that the server builds and hashes to compare against the signature on the presigned URL. So to avoid getting the error, you need to make sure that the string built by the aws-sdk is the same as the one built by the server.
After some digging about, I eventually found the code responsible for creating the string to hash in the aws-sdk. It is located (as of version 2.7.12) in:
node_modules/aws-sdk/lib/signers/s3.js
Down the bottom at line 168 there is a sign method:
sign: function sign(secret, string) {
return AWS.util.crypto.hmac(secret, string, 'base64', 'sha1');
}
If you put a console.log in there, string is what you're after. Once you make the string that gets passed into this method the same as the contents of StringToSign in the error message coming back from Amazon, the heavens will open and your files will flow effortlessly into your bucket.
On my server running node.js, I originally created my presigned URL like this:
var AWS = require('aws-sdk');
var s3 = new AWS.S3(options = {
endpoint: 'https://s3-eu-west-1.amazonaws.com',
accessKeyId: "ACCESS_KEY",
secretAccessKey: "SECRET_KEY"
});
var params = {
Bucket: 'bucketName',
Key: imageName,
Expires: 60
};
var signedUrl = s3.getSignedUrl('putObject', params);
//return signedUrl
This produced a signing string like this, similar to the OP's:
PUT
1481366396
/bucketName/fileName.jpg
On the client side, I used this presigned URL with cordova-plugin-file-transfer like so (I'm using Ionic 2 so the plugin is wrapped in their native wrapper):
let success = (result: any) : void => {
console.log("upload success");
}
let failed = (err: any) : void => {
let code = err.code;
alert("upload error - " + code);
}
let ft = new Transfer();
var options = {
fileName: filename,
mimeType: 'image/jpeg',
chunkedMode: false,
httpMethod:'PUT',
encodeURI: false,
};
ft.upload(localDataURI, presignedUrlFromServer, options, false)
.then((result: any) => {
success(result);
}).catch((error: any) => {
failed(error);
});
Running the code produced the signature doesn't match error, and the string in the <StringToSign> element looks like this:
PUT
multipart/form-data; boundary=+++++org.apache.cordova.formBoundary
1481366396
/bucketName/fileName.jpg
So we can see that cordova-plugin-file-transfer has added in its own Content-Type header which has caused a discrepancy in the signing strings. In the docs relating to the options object that get passed into the upload method it says:
headers: A map of header name/header values. Use an array to specify more than one value. On iOS, FireOS, and Android, if a header named Content-Type is present, multipart form data will NOT be used. (Object)
so basically, if no Content-Type header is set it will default to multipart form data.
Ok so now we know the cause of the problem, it's a pretty simple fix. On the server side I added a ContentType to the params object passed to the S3 getSignedUrl method:
var params = {
Bucket: 'bucketName',
Key: imageName,
Expires: 60,
ContentType: 'image/jpeg' // <---- content type added here
};
and on the client added a headers object to the options passed to cordova-plugin-file-transfer's upload method:
var options = {
fileName: filename,
mimeType: 'image/jpeg',
chunkedMode: false,
httpMethod:'PUT',
encodeURI: false,
headers: { // <----- headers object added here
'Content-Type': 'image/jpeg',
}
};
and hey presto! The uploads now work as expected.
I run into such issues with this plugin
The only working way I found to upload a file with a signature is the method of Christophe Coenraets : http://coenraets.org/blog/2013/09/how-to-upload-pictures-from-a-phonegap-app-to-amazon-s3/
With this method you will be able to upload your files using the cordova-plugin-file-transfer
First, I wanted to use the aws-sdk on my server to sign with getSignedUrl()
It returns the signed link and you only have to upload to it.
But, using the plugin it always end with 403 : signatures don't match
It may be related to the content length parameter but I didn't found for now a working solution with aws-sdk and the plugin