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

Cannot compose createUploadLink #63

Closed
flemwad opened this issue Feb 1, 2018 · 17 comments
Closed

Cannot compose createUploadLink #63

flemwad opened this issue Feb 1, 2018 · 17 comments
Labels

Comments

@flemwad
Copy link

flemwad commented Feb 1, 2018

This was mentioned in #35 already, but I'm still experiencing the same issue when trying to compose any other link with createUploadLink.

My code:

function omitTypename(key, value) {
    return key === '__typename' ? undefined : value
}

const cleanTypenameFieldLink = new ApolloLink((operation, forward) => {
    if (operation.variables) {
        operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename)
    }

    return forward(operation)
});

const uri = 'http://localhost:8080';
const uploadLink = createUploadLink({ uri });
const appLink = cleanTypenameFieldLink.concat(uploadLink);

// Apollo client
const client = new ApolloClient({
    link: appLink,
    cache: new InMemoryCache(),
    credentials: 'same-origin'
});

I've tried a combination of:

1)
concat(uploadLink, cleanTypenameFieldLink)
uploadLink.concat(cleanTypenameFieldLink)

my custom cleanTypenameFieldLink doesn't run, but the file will upload

The order above throws an apollo-link warning:
You are calling concat on a terminating link, which will have no effect

2)
concat(cleanTypenameFieldLink, uploadLink),
cleanTypenameFieldLink.concat(uploadLink);
While this seems to work, but the upload promise is undefined when it hits the server. Which I'm assuming means the clientUploadLink never was ran when last.

3)
I've also tried the fix mentioned in the bottom of the issue of referencing the build index.js directly, to no avail.

4)
Assumed my custom link wasn't working and tried something simple like HttpLink, I had the same results as 1 and 2 from above.

In that issue it was mentioned this might be a .mjs and CRA issue, but I've ejected my CRA and confirmed I have the merged code from the mentioned CRA PR in #35. Which just seemed to be CRA support for .mjs extensions.

If I've missed anything please let me know! Everything has worked great otherwise, so thank you.

@jaydenseric
Copy link
Owner

That error makes sense, it looks like you are concatenating the links in the wrong order. apollo-upload-client is a "terminating" link; that means no link can be concatenated after because it is the end of the road for sending the request off.

@flemwad
Copy link
Author

flemwad commented Feb 1, 2018

As mentioned in 1) and 2) I've tried it both ways. With uploadLink last as well.

It appears to me what's happening is:

When I don't compose it and just use the straight up uploadLink

const uploadLink = createUploadLink({ uri });

const client = new ApolloClient({
    link: uploadLink,
    cache: new InMemoryCache(),
    credentials: 'same-origin' //TODO: Make this more stringent on prod
});

I'm getting an upload "Promise" on the server side, which then lets me resolve the stream from it:

upload:
   Promise {
     { stream: [Object],
     filename: 'cool2.png',
     mimetype: 'image/png',
     encoding: '7bit' } }

But, when I compose the link with my custom link:

import cleanTypenameFieldLink from './utils/cleanTypenameFieldLink';
const uploadLink = createUploadLink({ uri });
const appLink = concat(cleanTypenameFieldLink, uploadLink);

// Apollo client
const client = new ApolloClient({
    link: appLink,
    cache: new InMemoryCache(),
    credentials: 'same-origin' //TODO: Make this more stringent on prod
});

The response on my server I just get a "preview" file upload, and no promise to resolve:

upload:
   { preview: 'blob:http://localhost:3333/e514498c-3031-403c-96ea-349b2bd120d7' }

I'm not sure if I did something wrong? But before I wrote my code depending on a promise being sent to the server. So that's breaking.

@dbelchev
Copy link

dbelchev commented Feb 1, 2018

@flemwad, I assume that you have other problem here, but according to Apollo-link docs you can compose the links as follows:
let link = ApolloLink.from([errorLink, middlewareLink, apolloUploadLink]);
The above is working for me in my project without any problem with apolloUploadLink.

@flemwad
Copy link
Author

flemwad commented Feb 1, 2018

@dbelchev I'm aware you can concat in that manner too. I tried that and it yielded the same result:

import cleanTypenameFieldLink from './utils/cleanTypenameFieldLink';
const uploadLink = createUploadLink({ uri });

// Apollo client
const client = new ApolloClient({
    link: ApolloLink.from([cleanTypenameFieldLink, uploadLink]),
    cache: new InMemoryCache(),
    credentials: 'same-origin' //TODO: Make this more stringent on prod
});

Which is a un-promised preview file on the server:

upload:
   { preview: 'blob:http://localhost:3333/3a4284ce-0ffd-4eed-b80b-3a6262d7ee3a' }

or upload simply ends up null as shown below.

The only thing I can think is that I'm using the scalar Upload type as a nested property on a larger input type at this point:

export const PhotoInput = `
    scalar Upload

    input PhotoInput {
        id: String!
        postName: String!
        whatToDo: String!
        unixTime: Int!
        bounty: Float
        upload: Upload!
        meta: PhotoMetaInput!
    }
`;

Here's what my object looks like before sending:
upload-client

Note that this input works when un-composed, and doesn't include the "__typename" prop.

Here's the same object console.logged in an extremely dumb link function showing that the photoInput upload property has changed:

export default new ApolloLink((operation, forward) => {
    console.log('after', operation);
    return forward(operation)
});

afterlink

At this point, depending on what the "dumb" link does, upload either ends up: null or just a preview blob, and I can't execute the promise on the server.

@flemwad
Copy link
Author

flemwad commented Feb 1, 2018

This was an issue with my own link. Closing, and I apologize for any confusion.

@flemwad flemwad closed this as completed Feb 1, 2018
@smithaitufe
Copy link

smithaitufe commented Feb 9, 2018

@flemwad Please how did you resolve your issue? If you don't mind, can you look at this #64

Currently, I can't even do a post to my server.

@kojuka
Copy link

kojuka commented Feb 26, 2018

yes @flemwad I'm getting the same thing. please let us know how you fixed it.

@g-borgulya
Copy link

I have exactly the same issue, no idea how to figure out the cause of the problem.
Any ideas plesae how to start solving such an issue? It seems it could be valuable for others as well.

Maybe it would be also very useful if someone could provide an example how to correctly create a complex link both on the client and using ssr, and how to use it if the graphql schema is a bit more complex.

@smithaitufe
Copy link

I decided to avoid this package. I send base64 of whatever I want to upload to the server.

@bennypowers
Copy link

I have a requestLink which routes requests depending on if they are subscriptions or queries, which is a terminating link. That means that I have to choose between using uploadLink and requestLink? Is it not possible to use this package with another terminating link?

@jaydenseric
Copy link
Owner

@bennypowers you can't have 2 terminating links, because, well, they are terminating. The last one will never happen! You can use the Apollo link .split() method to create a fork on some sort of logic to choose which link to terminate with.

@bennypowers
Copy link

My implementation, based on @Zn4rK's work, for avoiding ' You are calling concat on a terminating link, which will have no effect` while still using Batching etc.

import { getMainDefinition } from 'apollo-utilities';
import { split } from 'apollo-link';
import { httpLink } from './httpLink.js';
import { wsLink } from './wsLink.js';
import { uploadLink } from './uploadLink.js';

const isFile = value => (
  (typeof File !== 'undefined' && value instanceof File) ||
  (typeof Blob !== 'undefined' && value instanceof Blob)
);

const isUpload = ({ variables }) =>
  Object.values(variables).some(isFile);

const isSubscriptionOperation = ({ query }) => {
  const { kind, operation } = getMainDefinition(query);
  return kind === 'OperationDefinition' && operation === 'subscription';
};

const requestLink = split(isSubscriptionOperation, wsLink, httpLink);

export const terminalLink = split(isUpload, uploadLink, requestLink);

@norbertkeri
Copy link

I think it would be worthwhile to mention in the README that you can use .split() in combination with upload-client, to keep batch working for your regular queries, I think that's what the major usecase people are looking for, not the batching of the uploads themselves.

@mehmetnyarar
Copy link

mehmetnyarar commented Mar 20, 2019

I have this issue currently and tried to solve it by using @bennypowers 's method. Since my mutations are sometimes objects, i've used lodash's isPlainObject method to determine the terminal link.

const requestLink = split(
  ({ query }) => {
    const { kind, operation }: any = getMainDefinition(query)
    console.log('requestLink', { query, kind, operation })
    return kind === 'OperationDefinition' && operation === 'subscription'
  },
  wsLink,
  httpLink
)
const uploadLink = createUploadLink({ uri: BACKEND_GQL })
const isFile = (value: any) => {
  if (isPlainObject(value)) return Object.values(value).map(isFile).includes(true)
  const isfile = typeof File !== 'undefined' && value instanceof File
  const isblob = typeof Blob !== 'undefined' && value instanceof Blob
  return isfile || isblob
}
const isUpload = ({ variables }) => Object.values(variables).some(isFile)
const terminalLink = split(isUpload, uploadLink, requestLink)
const link = ApolloLink.from([
  authLink,
  errorLink,
  terminalLink
])
export const client = new ApolloClient({
  link,
  cache
})

The topic was closed and a little bid old. Therefore I'd like to ask if there is any update to this or any other solution, or more easy way? Because for each request I have make this decision, and 99% of the operations are not related to upload.

@Tanapruk
Copy link

Tanapruk commented Apr 21, 2019

The omitTypename with JSON.parse is too aggressive that it deleted the File, so when forwarding to the uploadLink, it disappeared.

function omitTypename(key, value) {
    return key === '__typename' ? undefined : value
}

const cleanTypenameFieldLink = new ApolloLink((operation, forward) => {
    if (operation.variables) {
        operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename)
    }

    return forward(operation)
});

My fix is to use a less aggressive omit __typename function with a check for file uploading first.

const isFile = value => (
  (typeof File !== 'undefined' && value instanceof File) ||
  (typeof Blob !== 'undefined' && value instanceof Blob)
);

//borrow from https://gist.github.com/Billy-/d94b65998501736bfe6521eadc1ab538
function omitDeep(value, key) {
  if (Array.isArray(value)) {
    return value.map((i) => omitDeep(i, key))
  } else if (typeof value === 'object' && value !== null && !isFile(value)) {
    return Object.keys(value).reduce((newObject, k) => {
      if (k == key) return newObject
      return Object.assign({ [k]: omitDeep(value[k], key) }, newObject)
    }, {})
  }
  return value
}


function createOmitTypenameLink() {
  return new ApolloLink((operation, forward) => {
    if (operation.variables) {
      operation.variables = omitDeep(operation.variables, '__typename')
    }
    return forward(operation)
  })
}

const uploadLink = createUploadLink({
  uri: GRAPHQL_URL
})

const createApolloClient = (cache = {}) => {
  return new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: typeof window !== 'undefined',
    cache: new InMemoryCache().restore(cache),
    link: ApolloLink.from([omitTypenameLink, uploadLink])
  })
}

@acomito
Copy link

acomito commented Dec 6, 2019

I'm trying to figure this one out. Would love some help if anyone notices an obvious blunder below


import {ApolloClient} from 'apollo-client';
import {ApolloLink} from 'apollo-link';
import {InMemoryCache} from 'apollo-cache-inmemory';
import fetch from 'isomorphic-fetch'; // https://github.com/apollographql/apollo-link/issues/513#issuecomment-368234260
import apolloLogger from 'apollo-link-logger';
import {authLink, httpLink} from './links';
import errorLink from './links/errorLink';
import {createUploadLink} from 'apollo-upload-client';

// create apollo cache
const cache = new InMemoryCache({});

const uploadLink = createUploadLink({
  uri: !process.env.REACT_APP_API_HOST,
  headers: {
    'keep-alive': 'true',
  },
});

let links = [authLink, errorLink, httpLink, uploadLink];

if (process.env.NODE_ENV === 'development') {
  links = [apolloLogger, authLink, errorLink, httpLink, uploadLink];
}

// create apollo-client instance
const client = new ApolloClient({
  link: ApolloLink.from(links),
  fetch,
  cache,
  connectToDevTools: process.env.NODE_ENV === 'development',
});

// export the client to be used by the app
export default client;

everything is working until I add uploadLink

UPDATE: All I had to do was remove my httpLink which I guess is doing the same thing as uploadLink

@fresonn
Copy link

fresonn commented Oct 25, 2022

@acomito, I got the same issue, but the solution was on README in installation section.
Quote:

Apollo Client can only have 1 terminating Apollo Link that sends the GraphQL requests; if one such as HttpLink is already setup, remove it.

It seems to better read the docs first for all of us 😂

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

No branches or pull requests