Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passthrough for binary files not working #305

Open
mydea opened this issue May 20, 2020 · 8 comments · May be fixed by #363
Open

Passthrough for binary files not working #305

mydea opened this issue May 20, 2020 · 8 comments · May be fixed by #363

Comments

@mydea
Copy link

mydea commented May 20, 2020

I just spent many hours trying to figure out why a binary file sent via an API was always corrupted, until I found out it was because of Pretender passthrough somehow bungling it up.

I searched and found this: #148, but as far as I see this cannot be set when using fetch().

@janmisek
Copy link

This is kind blocking. Without pretender fetch('/image.png') returns corrent blob with correct size. With pretender it does not.

@ankology
Copy link

ankology commented Sep 1, 2020

Any solution found @mydea ? I made a entire app with pretender and found that issue too..

@mydea
Copy link
Author

mydea commented Sep 2, 2020

No, sadly not!

@ankology
Copy link

ankology commented Sep 2, 2020

I started to look at pretender source code and have found a solution. In create-passthrough.ts#L28 just check blob and arraybuffer response types, this solves the main issue with blob's. I found another issues, some events are firing twice and when using axios the event "readystatechange" are not firing properly depending on implementation. So, basically needs to replace the createPassthrough function from the file create-passthrough.ts with the changes (Changes explained prefixed by ### in comments):

function createPassthrough(fakeXHR, nativeXMLHttpRequest) {
   // event types to handle on the xhr
   var evts = ['error', 'timeout', 'abort', 'readystatechange'];
   // event types to handle on the xhr.upload
   var uploadEvents = [];
   // properties to copy from the native xhr to fake xhr
   var lifecycleProps = [
       'readyState',
       'responseText',
       'response',
       'responseXML',
       'responseURL',
       'status',
       'statusText',
   ];
   var xhr = (fakeXHR._passthroughRequest = new nativeXMLHttpRequest());
   xhr.open(fakeXHR.method, fakeXHR.url, fakeXHR.async, fakeXHR.username, fakeXHR.password);
   // if (fakeXHR.responseType === 'arraybuffer') {
   //### check for blob either
   if (~['blob', 'arraybuffer'].indexOf(fakeXHR.responseType)) {
       lifecycleProps = ['readyState', 'response', 'status', 'statusText'];
       xhr.responseType = fakeXHR.responseType;
   }
   // use onload if the browser supports it
   if ('onload' in xhr) {
       evts.push('load');
   }

   // add progress event for async calls
   // avoid using progress events for sync calls, they will hang https://bugs.webkit.org/show_bug.cgi?id=40996.
   // if (fakeXHR.async && fakeXHR.responseType !== 'arraybuffer') {
   //### just check for async request
   if (fakeXHR.async) {
       evts.push('progress');
       uploadEvents.push('progress');
   }
   // update `propertyNames` properties from `fromXHR` to `toXHR`
   function copyLifecycleProperties(propertyNames, fromXHR, toXHR) {
       for (var i = 0; i < propertyNames.length; i++) {
           var prop = propertyNames[i];
           if (prop in fromXHR) {
               toXHR[prop] = fromXHR[prop];
           }
       }
   }
   // fire fake event on `eventable`
   function dispatchEvent(fakeXHR, eventType, event) {

     //### only dispatchEvent, not manual firing events
     fakeXHR.dispatchEvent(event);

     // if (fakeXHR['on' + eventType]) {
     //
     //   // fakeXHR['on' + eventType](event);
     // }
   }

   // set the on- handler on the native xhr for the given eventType
   function createHandler(eventType) {

       //### delete manual addition of events and re-adding the listener to dispatchEvent always work properly
       const fakeEventKey = 'on'+eventType;

       if(fakeXHR[fakeEventKey]){

         const fn = fakeXHR[fakeEventKey];
         delete fakeXHR[fakeEventKey];

         fakeXHR.addEventListener(eventType, fn);
       }

       xhr.addEventListener(eventType, function (event) {
         copyLifecycleProperties(lifecycleProps, xhr, fakeXHR);
         dispatchEvent(fakeXHR, eventType, event);
       });
       //###

       // xhr['on' + eventType] = function (event) {
       //     copyLifecycleProperties(lifecycleProps, xhr, fakeXHR);
       //     dispatchEvent(fakeXHR, eventType, event);
       // };
   }
   // set the on- handler on the native xhr's `upload` property for
   // the given eventType

   function createUploadHandler(eventType) {

     // if (xhr.upload && fakeXHR.upload && fakeXHR.upload['on' + eventType]) {
     if (xhr.upload && fakeXHR.upload) {

       //### delete manual addition of events and re-adding the listener to dispatchEvent always work properly
       const fakeEventKey = 'on'+eventType;

       if(fakeXHR.upload[fakeEventKey]){

         const fn = fakeXHR.upload[fakeEventKey];
         delete fakeXHR.upload[fakeEventKey];

         fakeXHR.upload.addEventListener(eventType, fn);
       }

       xhr.upload.addEventListener(eventType, function (event) {
         dispatchEvent(fakeXHR.upload, eventType, event);
       });
       //###

       // xhr.upload['on' + eventType] = function (event) {
       //     dispatchEvent(fakeXHR.upload, eventType, event);
       // };
       }
   }
   var i;
   for (i = 0; i < evts.length; i++) {
       createHandler(evts[i]);
   }
   for (i = 0; i < uploadEvents.length; i++) {
       createUploadHandler(uploadEvents[i]);
   }
   if (fakeXHR.async) {
       xhr.timeout = fakeXHR.timeout;
       xhr.withCredentials = fakeXHR.withCredentials;
   }
   for (var h in fakeXHR.requestHeaders) {
       xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]);
   }
   return xhr;
}

And my tests code:

import axios from 'axios';
import Pretender from 'pretender';

const server = new Pretender(function() {

    this.get('/test', () => {

        return [200, {}, {data: {foo: 'bar'}}]
    });
});

server.unhandledRequest = function(verb, path, request) {

    const xhr = request.passthrough();

    xhr.onloadend = (ev) => {
        console.warn(`Response for ${path}`, {
            verb,
            path,
            request,
            responseEvent: ev,
        })
    };
};

const urls = [

    'https://static.vecteezy.com/system/resources/thumbnails/000/246/312/original/mountain-lake-sunset-landscape-first-person-view.jpg',
    'https://speed.hetzner.de/100MB.bin',
    'https://speed.hetzner.de/1GB.bin'
];

const url = urls[0];
const postUrl = `https://postman-echo.com/post`;

function buildPostFormData(){

    //Some form size to show upload progress process better
    const formData = new FormData();

    for(let x = 0; x < 5000; x++){

        formData.append(x, 'My data ' + x);
    }

    return formData;
}

const tests = {

    native: {

        get(){

            const xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'blob';
            xhr.onprogress = function(e){

                console.log('ondownloadprogress', e);
            };

            xhr.onload = function (event) {

                console.log('load done', xhr.response);

                if(url === urls[0]) {

                    const urlCreator = window.URL || window.webkitURL;
                    const imageUrl = URL.createObjectURL(this.response);
                    const el = document.createElement('img');
                    el.src = imageUrl;
                    document.body.append(el);
                }
            }

            xhr.send();
        },
        post(){

            const xhr = new XMLHttpRequest();
            xhr.open('POST', postUrl, true);

            xhr.onprogress = (e) => {

                console.log('ondownloadprogress', e)
            }

            xhr.upload.onprogress = (e) => {

                console.log('onuploadprogress', e)
            };

            // xhr.upload.addEventListener('progress', (e) => {
            //
            //     console.log('onuploadprogress 2', e)
            //
            // }, false);
            //
            // xhr.upload.addEventListener('progress', (e) => {
            //
            //     console.log('onuploadprogress 3', e)
            //
            // }, false);

            xhr.onload = () => {

                console.log('done upload')
            }

            xhr.send(buildPostFormData());
        }
    },
    axios: {

        get(){

            axios.get(url, {

                responseType: 'blob',
                onDownloadProgress(e){

                    console.log('ondownloadprogress', e)
                },
                onUploadProgress(e){

                    console.log('onuploadprogress', e);
                }

            }).then(result => {

                console.log(result);

                if(url === urls[0]) {

                    const urlCreator = window.URL || window.webkitURL;
                    const imageUrl = URL.createObjectURL(result.data);
                    const el = document.createElement('img');
                    el.src = imageUrl;
                    document.body.append(el);
                }

            }).catch(e => {

                console.log(e);
            })
        },
        post(){

            axios.post(

                postUrl,
                buildPostFormData(),
                {
                    headers: {

                        'Content-Type': 'multipart/form-data'
                    },
                    onDownloadProgress(e){

                        console.log('ondownloadprogress', e)
                    },
                    onUploadProgress(e){

                        console.log('onuploadprogress', e);
                    }

            }).then(res => {

                console.log(res);

            }).catch(e => {

                console.log(e);
            });
        }
    }
};

tests.native.get();

One more thing. In order to download files from web in localhost, its going to be needed to disabled browser CORS policy, in case of chrome see Disable CORS Chrome.

Hope it helps @mydea! Regards.

@xg-wang
Copy link
Member

xg-wang commented Apr 24, 2021

@ankology nice finding! Would you mind creat a PR with your change?

@HarshRohila
Copy link

I was facing this issue when using mirage js with pdf js
I found where pretender storing original fetch, so created a workaround function

export default async function useNativeFetch(anyFunc) {
	const savedFetch = window.fetch;
	window.fetch = mirageServer.pretender._nativefetch;

	const res = await anyFunc();

	window.fetch = savedFetch;
	return res;
}

using it like this

const pdf = await useNativeFetch(
	() => pdfjsLib.getDocument(pdfUrl).promise
);

@miccoh1994
Copy link

miccoh1994 commented Dec 1, 2021

#305 (comment)

Would be fantatsic if this was made into a PR, major bummer

@Techn1x Techn1x linked a pull request Jan 29, 2024 that will close this issue
@Techn1x
Copy link

Techn1x commented Jan 29, 2024

PR here with Ankology's fix
#363

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants