pretenderjs / pretender

A mock server library with a nice routing DSL
MIT License
1.26k stars 158 forks source link

Passthrough for binary files not working #305

Open mydea opened 4 years ago

mydea commented 4 years ago

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: https://github.com/pretenderjs/pretender/pull/148, but as far as I see this cannot be set when using fetch().

janmisek commented 4 years ago

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

ankology commented 4 years ago

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

mydea commented 4 years ago

No, sadly not!

ankology commented 4 years ago

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 commented 3 years ago

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

HarshRohila commented 3 years ago

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 commented 2 years ago

https://github.com/pretenderjs/pretender/issues/305#issuecomment-685815226

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

Techn1x commented 9 months ago

PR here with Ankology's fix https://github.com/pretenderjs/pretender/pull/363