I'm trying to use a Cloudflare Worker to proxy a POST request to another server.
It is throwing a JS exception – by wrapping in a try/catch blog I've established that the error is:
TypeError: A request with a one-time-use body (it was initialized from a stream, not a buffer) encountered a redirect requiring the body to be retransmitted. To avoid this error in the future, construct this request from a buffer-like body initializer.
I would have thought this could be solved by simply copying the Response so that it's unused, like so:
return new Response(response.body, { headers: response.headers })
That's not working. What am I missing about streaming vs buffering here?
addEventListener('fetch', event => {
var url = new URL(event.request.url);
if (url.pathname.startsWith('/blog') || url.pathname === '/blog') {
if (reqType === 'POST') {
event.respondWith(handleBlogPost(event, url));
} else {
handleBlog(event, url);
}
} else {
event.respondWith(fetch(event.request));
}
})
async function handleBlog(event, url) {
var newBlog = "https://foo.com";
var originUrl = url.toString().replace(
'https://www.bar.com/blog', newBlog);
event.respondWith(fetch(originUrl));
}
async function handleBlogPost(event, url) {
try {
var newBlog = "https://foo.com";
var srcUrl = "https://www.bar.com/blog";
const init = {
method: 'POST',
headers: event.request.headers,
body: event.request.body
};
var originUrl = url.toString().replace( srcUrl, newBlog );
const response = await fetch(originUrl, init)
return new Response(response.body, { headers: response.headers })
} catch (err) {
// Display the error stack.
return new Response(err.stack || err)
}
}
A few issues here.
First, the error message is about the request body, not the response body.
By default, Request and Response objects received from the network have streaming bodies -- request.body and response.body both have type ReadableStream. When you forward them on, the body streams through -- chunks are received from the sender and forwarded to the eventual recipient without keeping a copy locally. Because no copies are kept, the stream can only be sent once.
The problem in your case, though, is that after streaming the request body to the origin server, the origin responded with a 301, 302, 307, or 308 redirect. These redirects require that the client re-transmit the exact same request to the new URL (unlike a 303 redirect, which directs the client to send a GET request to the new URL). But, Cloudflare Workers didn't keep a copy of the request body, so it can't send it again!
You'll notice this problem doesn't happen when you do fetch(event.request), even if the request is a POST. The reason is that event.request's redirect property is set to "manual", meaning that fetch() will not attempt to follow redirects automatically. Instead, fetch() in this case returns the 3xx redirect response itself and lets the application deal with it. If you return that response on to the client browser, the browser will take care of actually following the redirect.
However, in your worker, it appears fetch() is trying to follow the redirect automatically, and producing an error. The reason is that you didn't set the redirect property when you constructed your Request object:
const init = {
method: 'POST',
headers: event.request.headers,
body: event.request.body
};
// ...
await fetch(originUrl, init)
Since init.redirect wasn't set, fetch() uses the default behavior, which is the same as redirect = "automatic", i.e. fetch() tries to follow redirects. If you want fetch() to use manual redirect behavior, you could add redirect: "manual" to init. However, it looks like what you're really trying to do here is copy the whole request. In that case, you should just pass event.request in place of the init structure:
// Copy all properties from event.request *except* URL.
await fetch(originUrl, event.request);
This works because a Request has all of the fields that fetch()'s second parameter wants.
What if you want automatic redirects?
If you really do want fetch() to follow the redirect automatically, then you need to make sure that the request body is buffered rather than streamed, so that it can be sent twice. To do this, you will need to read the whole body into a string or ArrayBuffer, then use that, like:
const init = {
method: 'POST',
headers: event.request.headers,
// Buffer whole body so that it can be redirected later.
body: await event.request.arrayBuffer()
};
// ...
await fetch(originUrl, init)
A note on responses
I would have thought this could be solved by simply copying the Response so that it's unused, like so:
return new Response(response.body, { headers: response.headers })
As described above, the error you're seeing is not related to this code, but I wanted to comment on two issues here anyway to help out.
First, this line of code does not copy all properties of the response. For example, you're missing status and statusText. There are also some more-obscure properties that show up in certain situations (e.g. webSocket, a Cloudflare-specific extension to the spec).
Rather than try to list every property, I again recommend simply passing the old Response object itself as the options structure:
new Response(response.body, response)
The second issue is with your comment about copying. This code copies the Response's metadata, but does not copy the body. That is because response.body is a ReadableStream. This code initializes the new Respnose object to contain a reference to the same ReadableStream. Once anything reads from that stream, the stream is consumed for both Response objects.
Usually, this is fine, because usually, you only need one copy of the response. Typically you are just going to send it to the client. However, there are a few unusual cases where you might want to send the response to two different places. One example is when using the Cache API to cache a copy of the response. You could accomplish this by reading the whole Response into memory, like we did with requests above. However, for responses of non-trivial size, that could waste memory and add latency (you would have to wait for the entire response before any of it gets sent to the client).
Instead, what you really want to do in these unusual cases is "tee" the stream so that each chunk that comes in from the network is actually written to two different outputs (like the Unix tee command, which comes from the idea of a T junction in a pipe).
// ONLY use this when there are TWO destinations for the
// response body!
new Response(response.body.tee(), response)
Or, as a shortcut (when you don't need to modify any headers), you can write:
// ONLY use this when there are TWO destinations for the
// response body!
response.clone()
Confusingly, response.clone() does something completely different from new Response(response.body, response). response.clone() tees the response body, but keeps the Headers immutable (if they were immutable on the original). new Response(response.body, response) shares a reference to the same body stream, but clones the headers and makes them mutable. I personally find this pretty confusing, but it's what the Fetch API standard specifies.
I am doing an API request follow by another one to a different server to which I need to pass a file.
Doing the first one is nice and easy. It looks something like this:
if (myFile) {
const data = new FormData()
data.append('myFile', myFile, myFile.name)
myFile = data
}
isomorphicFetch(`${MY_ENDPOINT}`, {
method: 'PATCH',
body: myFile
})
Now, in the server side, I need to pass this into another server. Which I am using superagent for. However I seem to be losing the file in the process. Here is what the code currently looks like:
const fileField = Object.keys(data).pop()
if (fileField === 'myFile') {
res = await request
.patch(`${MY_OTHER_ENDPOINT}`)
.send(data)
.query(query)
}
Take note that data is the body of the previous request, and the FormData object is displayed as an empty object, so I am not sure what to do from here.
On my other server, my file comes back as undefined, however if I do the request straight from the client, it goes through as expected. So how can I forward the FormData object from one server app to the other?
Files are typically posted to servers as part of a Multipart request.
Superagent supports multipart requests like so:
request
.post('/upload')
.attach('image1', 'path/to/felix.jpeg')
.attach('image2', imageBuffer, 'luna.jpeg')
.field('caption', 'My cats')
.then(callback);
Additional information can be found in their documentation:
Superagent Docs - Multipart requests
Specific to your example, you want to make sure that your server is receiving the file and then using the attach function to put the file in your request.
I've put together the following code from learning about the fetch API. I am trying to replace AJAX and this looks wonderful so far.
Main Question:
According to the Fetch API documentation...
A fetch() promise will reject with a TypeError when a network error is
encountered or CORS is misconfigured on the server side, although this
usually means permission issues or similar — a 404 does not constitute
a network error, for example.
Having the 3 technologies working together...
If I disable the Web Server I get:
NetworkError when attempting to fetch resource.
Wonderful. That works great.
If I disable MySQL I get my custom error from PHP:
MySQL server down?
Wonderful. That works great.
If I disable PHP I get exactly nothing because the only way I can think of to pass through the Web Server request and trigger an error at the PHP level is with a... timeout.
After some research, I don't think there is a timeout option... at least not yet.
How could I implement it in the code below?
// CLICK EVENT
$( "#btn_test" ).on('click', function() {
// Call function
test1();
});
function test1() {
// json() - Returns a promise that resolves with a JSON object.
function json_response(response) {
// Check if response was ok.
if(response.ok) {
return response.json()
}
}
// data - Access JSON data & process it.
function json_response_data(data) {
console.log('json_response_data: ', data);
}
// URL to post request to...
var url = 'data_get_json_select_distinct_client.php';
// Sample serializeArray() from html form data.
// <input type="text" name="CLIENT_ID" value="1000">
var post_data = [{
"name":"CLIENT_ID",
"value":"1000"
}];
// STRINGIFY
post_data = JSON.stringify(post_data);
// FETCH
fetch(url, {
method: 'post',
headers: new Headers({'Content-Type': 'application/json; charset=utf-8'}),
body: post_data
})
// VALID JSON FROM SERVER?
.then(json_response)
// ACCESS JSON DATA.
.then(json_response_data)
// ERROR.
.catch(function(error) {
console.log('Web server down?: ', error.message);
});
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<button type="button" id="btn_test">FETCH RECORD</button>
Your server should be returning some sort of 5xx error code, should there be a problem server-side. This error is catchable in your JS.
I am using jasmine-ajax to mock $.ajax get calls. In my actual code I send some parameters through the data options.
var request = $.ajax("/users", {
data: {id:"1"},
});
but in my tests jasmine.Ajax.requests.mostRecent().url returns /users?id=1 and jasmine.Ajax.requests.mostRecent().data() returns {}. Is there a way to make the url return /users and data return {id:"1"} to make my testing life easier?
When using GET method for making a request, the query string is sent like this /users?id=1. But you should use POST method if you want to make your "testing life easier".
var request = $.ajax("/users", {
method: "POST",
data: {id:"1"},
});
See the resulting specs in this jsfiddle:
https://jsfiddle.net/EduardoRG/49ufpe3b/
Hello first of all thanks for your support,
I getting started with angular and I am trying to use conmsume data from an API for my app. I am having a few problems with this.
First of all CORS:
To run local http server I am using the one that comes with node.js (using http-server command).
I am using http://www.mocky.io/ to test the app. I've generated differents (with headers I've found around the net that are supposed to fix it) response there to try to fix CORS (always getting preflight error) but nothing seems to work.
I have added this to my save method (inside a factory):
save: {
method: 'POST',
headers: {
'Access-Control-Allow-Origin': '*'
}
}
If I use a Chrome extension called CORS I can bypass that and receive response but then I am not able to manage the promise and get the data inside the response. I would like to be able to show the response's json on the view.
$scope.submitForm = function() {
var promise = null;
promise = CheckFactory.save($scope.partner).$promise;
$scope.result = promise.data;
}
This functions sends the data from the form to the factory and perform the request but then I am lost and do not know how to manage the data I need from the response.
Thanks in advance :)
Basically you need to put .then function over your save method call promise. So that will call .then function's once data save request gets completed.
$scope.submitForm = function() {
CheckFactory.save($scope.partner).$promise
//it will called success callback when save promise resolved.
.then(function(data){ //success
$scope.result = data;
}, function(error){ //error
});
}