-
-
Notifications
You must be signed in to change notification settings - Fork 156
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
Upload Progress - revisiting #88
Comments
I just threw this together: function customFetch(url, opts = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(opts.method || 'get', url)
for (let k in opts.headers || {}) xhr.setRequestHeader(k, opts.headers[k])
xhr.onload = e =>
resolve({
ok: true,
text: () => Promise.resolve(e.target.responseText),
json: () => Promise.resolve(JSON.parse(e.target.responseText))
})
xhr.onerror = reject
if (xhr.upload)
xhr.upload.onprogress = event =>
console.log(`${event.loaded / event.total * 100}% uploaded`)
xhr.send(opts.body)
})
} And it works: Keep in mind that createUploadLink({
uri: process.env.API_URI,
fetch: typeof window === 'undefined' ? global.fetch : customFetch
}) I don't intend to try to support progress with an official API, but good luck experimenting! |
@jaydenseric this is amazingly helpful. thank you so much. I don't know why, but I couldn't get the onload resolve object setup correctly. The last thing I'm struggling with is how you would approach getting access to this progress callback outside of the Apollo Link (i.e. in a react component)? |
I was able to solve this with the following:
And my custom fetch looks like this:
|
@justinlevi Can you share me your full source code? Thanks! |
FYI for anyone coming across this, I got upload progress bars working by using Axios instead of fetch. To make the request, I'm doing:
Then for my httpLink, I'm using:
The only gotcha was a minor bug in lifeomic/axios-fetch that prevented multipart requests from working, so temporarily using a fork at bouldercare/axios-fetch. |
This is a slightly modified version that I am using, it also allows canceling a request. const parseHeaders = (rawHeaders: any) => {
const headers = new Headers();
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
const preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, " ");
preProcessedHeaders.split(/\r?\n/).forEach((line: any) => {
const parts = line.split(":");
const key = parts.shift().trim();
if (key) {
const value = parts.join(":").trim();
headers.append(key, value);
}
});
return headers;
};
export const uploadFetch = (url: string, options: any) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
const opts: any = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || "")
};
opts.url =
"responseURL" in xhr
? xhr.responseURL
: opts.headers.get("X-Request-URL");
const body = "response" in xhr ? xhr.response : (xhr as any).responseText;
resolve(new Response(body, opts));
};
xhr.onerror = () => {
reject(new TypeError("Network request failed"));
};
xhr.ontimeout = () => {
reject(new TypeError("Network request failed"));
};
xhr.open(options.method, url, true);
Object.keys(options.headers).forEach(key => {
xhr.setRequestHeader(key, options.headers[key]);
});
if (xhr.upload) {
xhr.upload.onprogress = options.onProgress;
}
options.onAbortPossible(() => {
xhr.abort();
});
xhr.send(options.body);
});
const customFetch = (uri: any, options: any) => {
if (options.useUpload) {
return uploadFetch(uri, options);
}
return fetch(uri, options);
}; const link = createUploadLink({
uri: "http://localhost:7001/graphql",
credentials: "same-origin",
fetch: customFetch as any
}); Usage Example: export const Default = withMp3FileUpload(({ mutate }) => {
const [file, setFile] = useState<null | File>(null);
const [progress, setProgress] = useState<number>(0);
useEffect(() => {
if (!mutate || !file) {
return;
}
let abort: any;
mutate({
variables: {
file
},
context: {
fetchOptions: {
useUpload: true,
onProgress: (ev: ProgressEvent) => {
setProgress(ev.loaded / ev.total);
},
onAbortPossible: (abortHandler: any) => {
abort = abortHandler;
}
}
}
}).catch(err => console.log(err));
return () => {
if (abort) {
abort();
}
};
}, [file]);
} |
|
The example from @n1ru4l works on Apollo Client v3. Thanks. const customFetch = (uri: any, options: any) => {
if (options.onProgress) {
// file upload
return uploadFetch(uri, options);
}
return fetch(uri, options);
}; |
For anyone stumbling across this in need of a typescript version: const parseHeaders = (rawHeaders: string): Headers => {
const headers = new Headers()
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
const preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, " ")
preProcessedHeaders.split(/\r?\n/).forEach((line: string) => {
const parts = line.split(":")
const key = parts.shift()?.trim()
if (key) {
const value = parts.join(":").trim()
headers.append(key, value)
}
})
return headers
}
type OnloadOptions = { status: number; statusText: string; headers: Headers } & Record<
string,
any
>
type AbortHandler = XMLHttpRequest["abort"]
type CustomFetchOptions = RequestInit & {
useUpload: boolean
onProgress: (ev: ProgressEvent) => void
onAbortPossible: (abortHandler: AbortHandler) => void
}
export const uploadFetch = (
url: URL | RequestInfo,
options: CustomFetchOptions
): Promise<Response> =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = () => {
const opts: OnloadOptions = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || ""),
}
opts.url =
"responseURL" in xhr ? xhr.responseURL : opts.headers.get("X-Request-URL")
const body = "response" in xhr ? xhr.response : (xhr as any).responseText
resolve(new Response(body, opts))
}
xhr.onerror = () => {
reject(new TypeError("Network request failed"))
}
xhr.ontimeout = () => {
reject(new TypeError("Network request failed"))
}
xhr.open(options.method || "", url as string, true)
Object.keys(options.headers as Headers).forEach((key) => {
const headerValue = options.headers
? (options.headers[key as keyof HeadersInit] as string)
: ""
xhr.setRequestHeader(key, headerValue)
})
if (xhr.upload) {
xhr.upload.onprogress = options.onProgress
}
options.onAbortPossible(() => xhr.abort())
xhr.send(options.body as XMLHttpRequestBodyInit | Document | null | undefined)
})
export const customFetch = (
uri: URL | RequestInfo,
options: CustomFetchOptions
): Promise<Response> => {
if (options.useUpload) {
return uploadFetch(uri, options)
}
return fetch(uri, options)
} Which can be used like: const cache = new InMemoryCache()
const link = new HttpLink({
uri,
fetch: customFetch,
})
export const client = new ApolloClient({
uri,
cache,
link,
}) |
The current solution seem not working anymore, any one have a solution for this ? On the apollo client v3.8.7 |
I still don't think this is possible with a straight fetch replacement. JakeChampion/fetch#89
#53
All attempts and research has failed for me at this point. Try to do a console.log of progress and I think the issues will be clear.
The text was updated successfully, but these errors were encountered: