Code Monkey home page Code Monkey logo

datasource-rest's Introduction

Apollo REST Data Source

This package exports a (RESTDataSource) class which is used for fetching data from a REST API and exposing it via GraphQL within Apollo Server.

RESTDataSource wraps an implementation of the DOM-style Fetch API such as node-fetch and adds the following features:

  • Two layers of caching:
    • An in-memory "request deduplication" feature which by default avoids sending the same GET (or HEAD) request multiple times in parallel.
    • An "HTTP cache" which provides browser-style caching in a (potentially shared) KeyValueCache which observes standard HTTP caching headers.
  • Convenience features such as the ability to specify an un-serialized object as a JSON request body and an easy way to specify URL search parameters
  • Error handling

Documentation

View the Apollo Server documentation for RESTDataSource for more high-level details and examples.

Usage

To get started, install the @apollo/datasource-rest package:

npm install @apollo/datasource-rest

To define a data source, extend the RESTDataSource class and implement the data fetching methods that your resolvers require. Data sources can then be provided via Apollo Server's context object during execution.

Your implementation of these methods can call convenience methods built into the RESTDataSource class to perform HTTP requests, while making it easy to build up query parameters, parse JSON results, and handle errors.

const { RESTDataSource } = require('@apollo/datasource-rest');

class MoviesAPI extends RESTDataSource {
  override baseURL = 'https://movies-api.example.com/';

  async getMovie(id) {
    return this.get(`movies/${encodeURIComponent(id)}`);
  }

  async getMostViewedMovies(limit = 10) {
    const data = await this.get('movies', {
      params: {
        per_page: limit.toString(), // all params entries should be strings
        order_by: 'most_viewed',
      },
    });
    return data.results;
  }
}

API Reference

RESTDataSource is designed to be subclassed in order to create an API for use by the rest of your server. Many of its methods are protected. These consist of HTTP fetching methods (fetch, get, put, post, patch, delete, and head) which your API can call, and other methods that can be overridden to customize behavior.

This README lists all the protected methods. In practice, if you're looking to customize behavior by overriding methods, reading the source code is the best option.

Constructor

The RESTDataSource takes in a DataSourceConfig which allows for overriding some default behavior.

Configuration Overrides

  • cache - Custom KeyValueCache implementation
  • fetch - Custom Fetcher implementation
  • logger - Custom Logger implementation that will replace all logging activity within RESTDataSource

To override the RESTDataSource, see the following example code:

const dataSource = new (class extends RESTDataSource {})({
  cache: customCache,
  fetch: customFetcher,
  logger: customLogger,
});

Properties

baseURL

Optional value to use for all the REST calls. If it is set in your class implementation, this base URL is used as the prefix for all calls. If it is not set, then the value passed to the REST call is exactly the value used. See also resolveURL.

class MoviesAPI extends RESTDataSource {
  override baseURL = 'https://movies-api.example.com/';

  // GET
  async getMovie(id) {
    return this.get(
      `movies/${encodeURIComponent(id)}` // path
    );
  }
}

RESTDataSource interprets the string passed to methods such as this.get() as an URL in exactly the same way that a browser interprets a link on a web page whose address is the same as this.baseURL. This may lead to slightly surprising behavior if this.baseURL has a non-empty path component:

  • If the string passed to a method such as this.get() starts with a slash, then it is resolved relative to the host of the base URL, not to the full base URL. That is, if this.baseURL is https://foo.com/a/b/c/, then this.get('d') resolves to https://foo.com/a/b/c/d, but this.get('/d') resolves to https://foo.com/d.
  • If the base URL has a path element and does not end in a slash, then the given path replaces the last element of the path. That is, if baseURL is https://foo.com/a/b/c, this.get('d') resolves to https://foo.com/a/b/d

In practice, this means that you should usually set this.baseURL to the common prefix of all URLs you want to access including a trailing slash, and you should pass paths without a leading slash to methods such as this.get().

If a resource's path starts with something that looks like an URL because it contains a colon and you want it to be added on to the full base URL after its path (so you can't pass it as this.get('/foo:bar')), you can pass a path starting with ./, like this.get('./foo:bar').

httpCache

This is an internal object that adds HTTP-header-sensitive caching to HTTP fetching. Its exact API is internal to this package and may change between versions.

Overridable methods

cacheKeyFor

By default, RESTDatasource uses the cacheKey option from the request as the cache key, or the request method and full request URL otherwise when saving information about the request to the KeyValueCache. Override this method to remove query parameters or compute a custom cache key.

For example, you could use this to use header fields or the HTTP method as part of the cache key. Even though we do validate header fields and don't serve responses from cache when they don't match, new responses overwrite old ones with different header fields. (For the HTTP method, this might be a positive thing, as you may want a POST /foo request to stop a previously cached GET /foo from being returned.)

requestDeduplicationPolicyFor

By default, RESTDataSource de-duplicates all concurrent outgoing GET (or HEAD) requests in an in-memory cache, separate from the KeyValueCache used for the HTTP response cache. It makes the assumption that two GET (or two HEAD) requests to the same URL made in parallel can share the same response. When the request returns, its response is delivered to each caller that requested the same URL concurrently, and then it is removed from the cache.

If a request is made with the same cache key (method + URL by default) but with an HTTP method other than GET or HEAD, deduplication of the in-flight request is invalidated: the next parallel GET (or HEAD) request for the same URL will make a new request.

You can configure this behavior in several ways:

  • You can change which requests are de-deduplicated and which are not.
  • You can tell RESTDataSource to de-duplicate a request against new requests that start after it completes, not just overlapping requests. (This was the poorly-documented behavior of RESTDataSource prior to v5.0.0.)
  • You can control the "deduplication key" independently from the KeyValueCache cache key.

You do this by overriding the requestDeduplicationPolicyFor method in your class. This method takes an URL and a request, and returns a policy object with one of three forms:

  • {policy: 'deduplicate-during-request-lifetime', deduplicationKey: string}: This is the default behavior for GET requests. If a request with the same deduplication key is in progress, share its result. Otherwise, start a request, allow other requests to de-duplicate against it while it is running, and forget about it once the request returns successfully.
  • {policy: 'deduplicate-until-invalidated', deduplicationKey: string}: This was the default behavior for GET requests in versions prior to v5. If a request with the same deduplication key is in progress, share its result. Otherwise, start a request and allow other requests to de-duplicate against it while it is running. All future requests with policy deduplicate-during-request-lifetime or deduplicate-until-invalidated with the same deduplicationKey will share the same result until a request is started with policy do-not-deduplicate and a matching entry in invalidateDeduplicationKeys.
  • { policy: 'do-not-deduplicate'; invalidateDeduplicationKeys?: string[] }: This is the default behavior for non-GET requests. Always run an actual HTTP request and don't allow other requests to de-duplicate against it. Additionally, invalidate any listed keys immediately: new requests with that deduplicationKey will not match any requests that currently exist in the request cache.

The default implementation of this method is:

protected requestDeduplicationPolicyFor(
  url: URL,
  request: RequestOptions,
): RequestDeduplicationPolicy {
  const method = request.method ?? 'GET';
  // Start with the cache key that is used for the shared header-sensitive
  // cache. Note that its default implementation does not include the HTTP
  // method, so if a subclass overrides this and allows non-GET/HEADs to be
  // de-duplicated it will be important for it to include (at least!) the
  // method in the deduplication key, so we're explicitly adding GET/HEAD here.
  const cacheKey = this.cacheKeyFor(url, request);
  if (['GET', 'HEAD'].includes(method)) {
    return {
      policy: 'deduplicate-during-request-lifetime',
      deduplicationKey: `${method} ${cacheKey}`,
    };
  } else {
    return {
      policy: 'do-not-deduplicate',
      // Always invalidate GETs and HEADs when a different method is seen on the same
      // cache key (ie, URL), as per standard HTTP semantics. (We don't have
      // to invalidate the key with this HTTP method because we never write
      // it.)
      invalidateDeduplicationKeys: [
        this.cacheKeyFor(url, { ...request, method: 'GET' }),
        this.cacheKeyFor(url, { ...request, method: 'HEAD' }),
      ],
    };
  }

To fully disable de-duplication, just always return do-not-duplicate. (This does not affect the HTTP header-sensitive cache.)

class MoviesAPI extends RESTDataSource {
  protected override requestDeduplicationPolicyFor() {
    return { policy: 'do-not-deduplicate' } as const;
  }
}
willSendRequest

This method is invoked at the beginning of processing each request. It's called with the path and request provided to fetch, with a guaranteed non-empty headers and params objects. If a Promise is returned from this method it will wait until the promise is completed to continue executing the request. See the intercepting fetches section for usage examples.

resolveURL

In some cases, you'll want to set the URL based on the environment or other contextual values rather than simply resolving against this.baseURL. To do this, you can override resolveURL:

import type { KeyValueCache } from '@apollo/utils.keyvaluecache';

class PersonalizationAPI extends RESTDataSource {
  override async resolveURL(path: string, _request: AugmentedRequest) {
    if (!this.baseURL) {
      const addresses = await resolveSrv(path.split("/")[1] + ".service.consul");
      this.baseURL = addresses[0];
    }
    return super.resolveURL(path);
  }
}
cacheOptionsFor

Allows setting the CacheOptions to be used for each request/response in the HTTPCache. This is separate from the request-only cache. You can use this to set the TTL to a value in seconds. If you return {ttl: 0}, the response will not be stored. If you return a positive number for ttl and the operation returns a 2xx status code, then the response will be cached, regardless of HTTP headers: make sure this is what you intended! (There is currently no way to say "only cache responses that should be cached according to HTTP headers, but change the TTL to something specific".) Note that if you do not specify ttl here, only GET requests are cached.

You can also specify cacheOptions as part of the "request" in any call to get(), post(), etc. Note that specifically head() calls are not cached at all, so this will have no effect for HEAD requests. This can either be an object such as {ttl: 1}, or a function returning that object. If cacheOptions is provided, cacheOptionsFor is not called (ie, this.cacheOptionsFor is effectively the default value of cacheOptions).

The cacheOptions function and cacheOptionsFor method may be async.

override cacheOptionsFor() {
  return {
    ttl: 1
  }
}
didEncounterError

Note: In previous versions of RESTDataSource (< v5), this hook was expected to throw the error it received (the default implementation did exactly that). This is no longer required; as mentioned below, the error will be thrown immediately after invoking didEncounterError.

You can implement this hook in order to inspect (or modify) errors that are thrown while fetching, parsing the body (parseBody()), or by the throwIfResponseIsError() hook. The error that this hook receives will be thrown immediately after this hook is invoked.

You can also throw a different error here altogether. Note that by default, errors are GraphQLErrors (coming from errorFromResponse).

parseBody

This method is called with the HTTP response and should read the body and parse it into an appropriate format. By default, it checks to see if the Content-Type header starts with application/json or ends with +json (just looking at the header as a string without using a Content-Type parser) and returns response.json() if it does or response.text() if it does not. If you want to read the body in a different way, override this. This method should read the response fully; if it does not, it could cause a memory leak inside the HTTP cache. If you override this, you may want to override cloneParsedBody as well.

cloneParsedBody

This method is used to clone a body (for use by the request deduplication feature so that multiple callers get distinct return values that can be separately mutated). If your parseBody returns values other than basic JSON objects, you might want to override this method too. You can also change this method to return its argument without cloning if your code that uses this class is OK with the values returned from deduplicated requests sharing state.

shouldJSONSerializeBody

By default, this method returns true if the request body is:

  • a plain object or an array
  • an object with a toJSON method (which isn't a Buffer or an instance of a class named FormData)

You can override this method in order to serialize other objects such as custom classes as JSON.

throwIfResponseIsError

After the body is parsed, this method checks a condition (by default, if the HTTP status is 4xx or 5xx) and throws an error created with errorFromResponse if the condition is met.

errorFromResponse

Creates an error based on the response.

catchCacheWritePromiseErrors

This class writes to the shared HTTP-header-sensitive cache in the background (ie, the write is not awaited as part of the HTTP fetch). It passes the Promise associated with that cache write to this method. By default, this method adds a catch handler to the Promise which writes any errors to console.error. You could use this to do different error handling, or to do no error handling if you trust all callers to use the fetch method and await httpCache.cacheWritePromise.

trace

This method wraps the entire processing of a single request; if the NODE_ENV environment variable is equal to development, it logs the request method, URL, and duration. You can override this to provide observability in a different manner.

HTTP Methods

The get method on the RESTDataSource makes an HTTP GET request and returns its parsed body. Similarly, there are methods built-in to allow for POST, PUT, PATCH, DELETE, and HEAD requests. (The head method returns the full FetcherResponse rather than the body because HEAD responses do not have bodies.)

class MoviesAPI extends RESTDataSource {
  override baseURL = 'https://movies-api.example.com/';

  // an example making an HTTP POST request
  async postMovie(movie) {
    return this.post(
      `movies`, // path
      { body: movie }, // request body
    );
  }

  // an example making an HTTP PUT request
  async newMovie(movie) {
    return this.put(
      `movies`, // path
      { body: movie }, // request body
    );
  }

  // an example making an HTTP PATCH request
  async updateMovie(movie) {
    return this.patch(
      `movies`, // path
      { body: { id: movie.id, movie } }, // request body
    );
  }

  // an example making an HTTP DELETE request
  async deleteMovie(movie) {
    return this.delete(
      `movies/${encodeURIComponent(movie.id)}`, // path
    );
  }
}

All of the HTTP helper functions (get, put, post, patch, delete, and head) accept a second parameter for setting the body, headers, params, cacheKey, and cacheOptions (and other Fetch API options).

Alternatively, you can use the fetch method. The return value of this method is a DataSourceFetchResult, which contains parsedBody, response, and some other fields with metadata about how the operation interacted with the cache.

Intercepting fetches

Data sources allow you to intercept fetches to set headers, query parameters, or make other changes to the outgoing request. This is most often used for authorization or other common concerns that apply to all requests. The constructor can be overridden to require additional contextual information when the class is instantiated like so:

class PersonalizationAPI extends RESTDataSource {
  private token: string;

  constructor(token: string) {
    super();
    this.token = token;
  }

  willSendRequest(path, request) {
    // set an authorization header
    request.headers['authorization'] = this.token;
    // or set a query parameter
    request.params.set('api_key', this.token);
  }
}

If you're using TypeScript, you can use the AugmentedRequest type to define the willSendRequest signature:

import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';

class PersonalizationAPI extends RESTDataSource {
  override baseURL = 'https://personalization-api.example.com/';

  private token: string;
  constructor(token: string) {
    super();
    this.token = token;
  }

  override willSendRequest(_path: string, request: AugmentedRequest) {
    request.headers['authorization'] = this.token;
  }
}

Processing Responses

Looking for didReceiveResponse? This section is probably interesting to you.

You might need to read or mutate the response before it's returned. For example, you might need to log a particular header for each request. To do this, you can override the public fetch method like so:

  class MyDataSource extends RESTDataSource {
    override async fetch<TResult>(
      path: string,
      incomingRequest: DataSourceRequest = {}
    ) {
      const result = await super.fetch(path, incomingRequest);
      const header = result.response.headers.get('my-custom-header');
      if (header) {
        console.log(`Found header: ${header}`);
      }
      return result;
    }
  }

This example leverages the default fetch implementation from the parent (super). We append our step to the promise chain, read the header, and return the original result that the super.fetch promise resolved to ({ parsedBody, response }).

Integration with Apollo Server

To give resolvers access to data sources, you create and return them from your context function. (The following example uses the Apollo Server 4 API.)

interface MyContext {
  movies: MoviesAPI;
  personalization: PersonalizationAPI;
}

const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers,
});

// The context function you provide to your integration should handle constructing your data sources on every request.
const url = await startStandaloneServer(server, {
  async context({ req }) { 
    return {
      moviesAPI: new MoviesAPI(),
      personalizationAPI: new PersonalizationAPI(req.headers['authorization']),
    };
  },
});

From our resolvers, we can access the data source from context and return the result:

const resolvers = {
  Query: {
    movie: async (_source, { id }, { moviesAPI }) => {
      return moviesAPI.getMovie(id);
    },
    mostViewedMovies: async (_source, _args, { moviesAPI }) => {
      return moviesAPI.getMostViewedMovies();
    },
    favorites: async (_source, _args, { personalizationAPI }) => {
      return personalizationAPI.getFavorites();
    },
  },
};

datasource-rest's People

Contributors

aaronmoat avatar dependabot[bot] avatar github-actions[bot] avatar glasser avatar hishamali81 avatar js-lowes avatar justinsomers avatar lotmek avatar peakematt avatar renovate[bot] avatar sean-y-x avatar smyrick avatar svc-secops avatar trevor-scheer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

datasource-rest's Issues

apollo-datasource-rest caching everything

apollo-datasource-rest has a HTTPCache that probably works fine, but it is never invoked because RESTDataSource caches the promise itself, the first time it is called (for GET requests).

This seems to defeat the purpose of having HTTPCache altogether, but looking at the Git history, a good deal of work was put into this memoization, so maybe there is a reason for it?

https://github.com/apollographql/apollo-server/blob/f25784d51130bbb9014b727e2f231e5a737f4a50/packages/apollo-datasource-rest/src/RESTDataSource.ts#L270

Feature request - be able to tell if API was resolved by memory, cache, or source

I would like to be able to tell what resolved an API call. This can currently be resolved by either the in-memory cache, and external cache, or the actual source. This is needed for a billing log I am needing to write and I accumulate the data as the request is being processed (in my datasource code). This could be a feature that is 'turned on' somehow, so that users that do not need the feature will not enable it.

Fix RESTDataSource fetching a path with a colon

RESTDataSource.resolveUrl doesn't work correctly for paths with a colon, e.g.

this.get('/foo:')

this throws

TypeError: Only absolute URLs are supported

The source code strips out the leading slash, which leads to the call new URL('foo:', 'http://example.com'), where foo: looks like an absolute URL without a host.

https://github.com/apollographql/apollo-server/blob/cd19cfeff260c4b948fb56f0b3b210a43858cc8c/packages/apollo-datasource-rest/src/RESTDataSource.ts#L74-L88

RESTDataSource, cannot easily throw error if response is 200

Right now the didReceiveResponse and errorFromResponse pair works fairly well with non-2xx statuses. Unfortunately there exists REST services (legacy) which return 200 OK with error object instead as body. If trying to check this in didReceiveResponse and then throwing error through errorFromResponse causes problem with the response.json() parsing, as you cannot call that one twice.

Either the parseBody should know better (processing time memoize) or errorFromResponse should accept body passed and use that instead of calling parseBody. This would allow passing already parsed body to be passed to super.didReceiveResponse.

Right now my solution is just to copy-paste (partially) code for of ApolloError object create from errorFromResponse to the custom didReceiveResponse.

RESTDataSource capability to turn off tracing

What:
Every HTTP request writes in the console an entry. The only way to turn it off is by changing the process.env.NODE_ENV

Issue:
We should be able to turn it off in development.

How:
RESTDataSource can have an option to enable or disable the tracing.

Where:
Exactly on that line: https://github.com/apollographql/apollo-server/blob/master/packages/apollo-datasource-rest/src/RESTDataSource.ts#L271

At the moment, it checks for "development" but it still should let the developer to turn it off.

Error solving URL dynamically RestDataSource

Version
APOLLO RESTDATASOUCE = apollo-datasource-rest": "^0.9.3",
APOLLO SERVER = "apollo-server-lambda": "^2.16.1",
TYPESCRIPT = ""typescript": "^4.0.2""

I trying follow the documentation steps to set a dynamic url based on my ENV (setted in context)
But its show warning message 'baseURL' is defined as a property in class 'RESTDataSource<Context>', but is overridden here in 'TransactionsAPI' as an accessor.

Snippet:

export default class TransactionsAPI extends RESTDataSource<Context> {
  constructor() {
    super();
  }

  get baseURL() {
    if (this.context.env === "development") {
      return "https://dev.api_example.com.br";
    } else if (this.context.env === "production") {
      return "https://prod.api_example.com.br";
    }
  }
  async myFunction() {
   return this.post(`/patch`)
  }
}

Im following this steps
http://apollographql.com/docs/apollo-server/data/data-sources/#resolving-urls-dynamically

Thank you!

RESTDataSource, need custom context per Request/Response

I'm currently refactoring some REST API to use RESTDataSource as the fetch implementation. Current implementation uses similar custom rest/fetch implementation, which is fairly okay but does have it's caveats and does not provide the caching features.

However it does provide way to pass custom context on request/response pair. This is used for (in RESTDataSource terms) the parseBody to do more detailed json data denormalization. I was hoping I could implement this just extending the RESTDataSource, but seems it's not that easy as the request/response pair is being detached from each other. The didReceiveResponse does link up a bit, but as how the node-fetch Request and Response are internally initialized, any custom additions to the init: RequestInit parameter are not passed through just with Request and Response objects.

Right now I see only three options for this.

  1. FEATURE REQUEST: RESTDataSource should have way to pass "request context" which is passed through the pipe down to parseBody. This would work then when extending from RESTDataSource
  2. Hackly solution I'm trying is to use local Map to store the data and link by "request-id" which is passed through the request headers and then copied in didReceiveResponse to response headers which can be accessed in parseBody.
  3. Don't try to extend RESTDataSource, just create wrapper class and use RESTDataSource as it-is for any fetches, post process the responses result data in the wrapper

Because lack of direct support for option 1, I'm a bit leaning towards the option 3, although the RESTDataSource code seems to be done extending in mind. So probably have to extend RESTDataSource for some customzation and then wrap it..

Other suggestions?

apollo-datasource-rest: omit undefined query params

We have moved from request-promise to apollo-datasource-rest module in our project. The request-promise module was doing the job for us by omitting the undefined variables inside a query param object.

For example if we had something like:

{ param1: 'value', param2: undefined }

the resulting URL was:

https://domain.com/test-url?param1=value

But in case of apollo-datasource-rest we are getting:

https://domain.com/test-url?param1=value&param2=undefined

Which is breaking the existing tests in our project. We can omit the property at our side but it would be great if it would omit the undefined query params automagically ;)

add HEAD requests in RESTDataSource Class

Some endpoints of my REST API can be requested via a HEAD request but head is not implemented yet.

export class MyFancyAPI extends RESTDataSource {
    async countFancyDataset() {
        const response = await this.head(`countFancyDataset`); // myFancyDataset: 413212  (in Response Header and body will be empty)
        ...
    }
}

[RESTDataSource]: allow per-request caching logic

RESTDataSource caches each GET request, and clears the cache for other HTTP verbs. Most of the time this is exactly what users need. However there are cases where users need to cache POST requests as if they were query-style requests, for example, if they are POSTing a graphql query, or if the resource they are requesting accepts POST bodies as configuration for some computation. In these cases, users would like to configure the caching behaviour on a per-request basis.

API Proposal

class MyDataSource extends RESTDataSource<MyContext> {
  /** @override */
  protected checkRequestCache<T = any>(
    request: Request,
    cacheKey: string,
    performRequest: () => Promise<T>,
  ): Promise<T> {
    if (request.url.includes('/my-posty-query-endpoint')) {
      let promise = this.memoizedResults.get(cacheKey);
      if (promise) return promise;
      promise = performRequest();
      this.memoizedResults.set(cacheKey, promise);
      return promise;
    } else {
      return super.checkRequestCache(request, cacheKey, performRequest);
    }
  }
}

Absent this feature, the user will have to override the entire fetch method, when she really only wants to modify the last 1/5 of its code. Moreover, fetch is private and can't be overridden, so users with this case seemingly have no recourse.


Alternate API Proposal

base class
class RESTDataSource {
  private async fetch<TResult>(
    init: RequestInit & {
      path: string;
      params?: URLSearchParamsInit;
    },
  ): Promise<TResult> {
    // ... yadda yadda ...
  
    const performRequest = async () => { /* ... */ };
  
    if (this.shouldCacheRequest(request)) {
      return this.cacheRequest(cacheKey, performRequest)
    } else {
      this.memoizedResults.delete(cacheKey);
      return performRequest();
    }
  }
  
  /** Override to configure caching behaviour per request */
  protected shouldCacheRequest(request: Request): boolean {
    return request.method === 'GET';
  }
  
  private cacheRequest<T = any>(cacheKey: string, performRequest: () => Promise<T>): Promise<T> {
    let promise = this.memoizedResults.get(cacheKey);
    if (promise) return promise;
  
    promise = performRequest();
    this.memoizedResults.set(cacheKey, promise);
    return promise;
  }
}

Usage:

protected shouldCacheRequest(request: Request): boolean {
  const { pathname } = new URL(request.url);
  switch (pathname) {
    case '/query-post':
    case '/definitely-cache-this':
      return true;
    default:
      return request.method === 'GET';
  }
}

[apollo-datasource-rest] Possible memory leak due to TTL failure

Description

We're using apollo-datasource-rest alongside apollo-server to manage external REST requests.

We noticed that using any TTL value when doing a request leads to several memory leaks.

Here is an allocation timeline of the server while using {cacheOptions: {ttl: 1}}
cx-live-snapshots-1s-memory-cache

Timeline using {cacheOptions: {ttl: -1}}
cx-live-snapshots-no-memory-cache

We used 1 for this example but any other value creates same leaks in our observations.

The memory usage is quite stable in case of TTL -1:
cx-no-memory-cache-monitoring

But still growing in case of TTL 1:
cx-1s-memory-cache-monitoring

Version Info

apollo-server: 2.13.0
apollo-datasource-rest: 0.9.0

Expected Behavior

When TTL = 1 I'm expecting cached requests to be removed from memory after 1 sec but the InMemoryLRUCache store is still growing in my case.
memory-store-detail

Steps

yarn i
yarn run build
node --inspect dist/index.js
# open chrome://inspect in chrome
# send queries

Here is the server we use:

# index.js
const server = new ApolloServer({
    schema: MySchema,
    dataSources: {
       myAPI: new MyAPI({baseURL: ...})
    },
    context: ...,
    cacheControl: {
      defaultMaxAge: 86400,
    },
  });

Datasource:

export default class MyAPI extends RESTDataSource {
  constructor(options) {
    super();
    this.baseURL = options.baseURL;
  }

  async getItem(id) {
    const apiResult = await this.get(
      `/MyEndpointPath/${id}`,
      {
        aGetParam: 'getParamValue',
      },
      { cacheOptions: { ttl: 1 } },
    );
  }
  return {
     name: apiResult.title
  };
}

Resolver

export default {
  Query: {
       getItem(_, args, context): object {
              return context.dataSources.myAPI.getItem(args.id);
         },
   }
}

Schema

extend type Query {
  getItem(id: String): Item
}

type Item @cacheControl(maxAge: 86400) {
    name: String
}

Is it expected behavior or did we miss a part of the configuration ?

[apollo-datasource-rest] Http cache semantics not honored as expected

Package version
"apollo-datasource-rest": "^0.6.11"

Expected behavior
When a GET request is made using RESTDataSource and a ttl value is provided, the response is cached for the given time.

const response = await this.get('some-path', {}, { cacheOptions: { ttl: 600 } });

Then a second request is made while the request is still cached, but the reload option is given:

const response = await this.get('some-path', {}, { cache: 'reload', cacheOptions: { ttl: 600 } });

I am expecting this will fetch the response from the datasource and then save the response to the cache with a ttl of 600.

Actual behaviour
The response from the second request is returned from the cache (and the cache isn't updated). I believe that this if statement here should check whether the request needs to be remade regardless of the ttlOverride from the first cached request.

Please let me know if this behavior is deliberate.

Thanks!

[apollo-datasource-rest] canBeRevalidated() should consider more headers than ETag

apollo-datasource-rest has a mechanism to extend the TTL of a specific cache entry if the response can be revalidated.

Right now the method checks only for the presence of the ETag header, but the library used to extract the revalidation headers has a more complex handling: https://github.com/kornelski/http-cache-semantics/blob/master/index.js#L517

It may be useful to extend the canBeRevalidated method to include at least a check for last-modified header presence.

RESTDataSource fail to fetch urn-like resources

This is a bug report.

  • Package: apollo-datasource-rest
  • Version: latest

Fetching resources that are url-like does not work as expected.

Unit test to reproduce:

it('fails with urn-like resource', async () => {
      const dataSource = new (class extends RESTDataSource {
        baseURL = 'https://api.example.com';

        getFoo() {
          return this.get('/urn:foo:1');
        }
      })();

      dataSource.httpCache = httpCache;

      fetch.mockJSONResponseOnce();

      await dataSource.getFoo();

      expect(fetch.mock.calls.length).toEqual(1);
      expect(fetch.mock.calls[0][0].url).toEqual('https://api.example.com/urn:foo:1');
    });

The issue is caused by a combination of URL behavior and normalization logic in
https://github.com/apollographql/apollo-server/blob/master/packages/apollo-datasource-rest/src/RESTDataSource.ts#L74-L88

A better approach would be to remove an ending / from baseURL and add a leading / to path. E.g.

protected resolveURL(request: RequestOptions): ValueOrPromise<URL> {
    const normalizedPath = request.path.startsWith('/')
      ? request.path
      : request.path.concat('/');
    
    const baseURL = this.baseURL;
    if (baseURL) {
      const normalizedBaseURL = baseURL.endsWith('/')
        ? baseURL.substring(0, baseURL.length - 1)
        : baseURL;
      return new URL(normalizedPath, normalizedBaseURL);
    } else {
      return new URL(normalizedPath);
    }
}

however it breaks a few existing tests.

Can't read set-cookie headers from backend server properly

I'm using apollo server as an orchestration layer which connects and gets data from other backend servers.
I need to forward the set-cookie headers from backend server. Works well if there's one set-cookie header but if there are multiple there's a problem.

const { RESTDataSource } = require("apollo-datasource-rest");

class MyDataSource extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://example.com';
  }

  didReceiveResponse(response, _request) {
    // here response.header is an Iterable and get method gives a string joining multiple header
    let cookies = response.headers.get("set-cookie");
    console.log(cookies);
    return super.didReceiveResponse(response, _request);
  }

  async resolverFunc(body) {
    // ...
  }
}

return type for response.headers.get is string | null but in this case it should be array.
if we console log response.headers we see that inside Symbol map the set-cookie header is actually an array but gets converted to string when trying to get the value using the get method.
Couldn't find a way to get the original array.

Feature request: apollo-datasource-rest transform output to minify cache

My app downloads a lot information from multiple external webpages. The problem is that one of them they doesn't have an API. So I making a simple GET request to their webpage but that request return a lot of HTML that I parse and extract like 10 characters. The problem is that cache stores everything and it's so much unnecessary information.

I was thinking if there was possible to have something like

const response = await this.get(
  '/some/url',
  { some: 'query' },
  {
    cacheOptions: {
      ttl: 60 * 60 * 24,
    },
    // Want something like this to be executed once for each request but not when using cache.
    transform: (body) => { 
      const $ = cheerio.load(body);
      return $('.some-selector').text(); // Only cache this.
    }
  },
);
response // Should equal $('.some-selector').text() result

It could also be good transform big json objects into smaller object or fix bad data structure that doesn't fit your application.

I don't want todo something like this because it's inefficient:

const response = await this.get(
  '/some/url',
  { some: 'query' },
  {
    cacheOptions: {
      ttl: 60 * 60 * 24,
    },
  },
);
const $ = cheerio.load(response);
const result = $('.some-selector').text();

Cacheing in progress events issue

Caching in progress requests issue
There appears to be a race condition between two simultaneous calls to the same url endpoint from separate queries. I outlined the situation below.

Setup:

  • Two resolvers that fetch data from the same url endpoint from a single datasource simultaneously.
  • The datasource extends RESTDataSource and uses the get() method to fetch the data and cache the url endpoint and resulting promise.

Intended outcome:

  • There should be a single request to the http server

Actual Outcome:

  • We see two requests to the http server
  • Since the apollo cache gets populated only when the promise is resolved, the second call canโ€™t use the cache. Both requests fetch the data. This is especially problematic with a slow url endpoint. It results in double the time.

The issue reproduced in the following project: https://github.com/kowther/CashingInProgressEventIssue

There is no true documentation/reference for Apollo-Datasource

the apollo-datasource package doesn't have any reference documentation on the docs site. The full-stack tutorial give a few "helpful implementation" tips for building your own, but it's far from an API reference, or even detailed enough to say what interface should be expected for a custom datasource.

I'm just as capable as reading source code as the next guy, but with all the "v3 is coming" and package distribution renaming and everything... having absolutely no api reference left me confused as to whether that package was being deprecated in favor of something else.

[apollo-datasource-rest] errorFromResponse doesn't handle 422 validation errors

I have a suggestion to add processing of 422 status response in errorFromResponse and throw UserInputError in case of it.

  protected async errorFromResponse(response: Response) {
    const message = `${response.status}: ${response.statusText}`;

    let error: ApolloError;
    if (response.status === 401) {
      error = new AuthenticationError(message);
    } else if (response.status === 403) {
      error = new ForbiddenError(message);
    // add next lines:
    } else if (response.status === 422) {
      error = new UserInputError(this.getValidationErrorMessage(), this.parseValidationErrors(response));
    } else {
      error = new ApolloError(message);
    }

    const body = await this.parseBody(response);

    Object.assign(error.extensions, {
      response: {
        url: response.url,
        status: response.status,
        statusText: response.statusText,
        body,
      },
    });

    return error;
  }

Apollo-datasource-rest GET query removes plus (+) sign and commas from URL

I am attempting to hit a REST endpoint with multiple URL parameter values concatenated via either "+" or "," - specifically a Wordpress server. But the Apollo Get datasource call removes both of them from the URL, with what I would assume is an attempt at making the URL safe. Is there an option to prevent it from converting + and , in a Get request.

My request:

let args = { per_page: 50,
  tags: '2551+2552',
  orderby: 'date',
  order: 'desc' }
result = await this.get("posts", args);

The URL Generated:

http://my-wordpress-site/wp-json/wp/v2/posts?per_page=50&tags=2551%2B2552&orderby=date&order=desc

It should be: http://my-wordpress-site/wp-json/wp/v2/posts?per_page=50&tags=2551+2552&orderby=date&order=desc

HTTPCache throwing SyntaxError on parsing

Package
[email protected]

Description
Apparently it can happen that an entry of the InMemoryLRUCache is an object, even though the entry is stringified before it is put in the cache. This causes this line to throw an error: line 56: const { policy: policyRaw, ttlOverride, body } = JSON.parse(entry);

Expected behavior
Check if the entry is an object so it does not have to be parsed and the code can continue.

Actual behavior
An error is thrown:

SyntaxError: Unexpected token o in JSON at position 1
[1]         at JSON.parse (<anonymous>)
[1]         at ContextHTTPCache.<anonymous> (C:\Users\***\node_modules\apollo-datasource-rest\src\HTTPCache.ts:48:9)
[1]         at Generator.next (<anonymous>)
[1]         at fulfilled (C:\Users\***\node_modules\apollo-datasource-rest\dist\HTTPCache.js:4:58)
[1]         at processTicksAndRejections (internal/process/task_queues.js:97:5)

apollographql/apollo-server#4353

RESTDataSource - cannot convert object to primitive

Hi,
I have question about RESTDataSource docs:
https://www.apollographql.com/docs/apollo-server/features/data-sources.html#HTTP-Methods

What have you passed as a movie in post/put/patch requests? Because it seems that you can't pass object there. You should JSON.strigify it first. Otherwise you will get "cannot convert object to primitive" from node-fetch. Am I right?

Here is my data source:

export default class DataSource extends BaseDataSource {
    uri = '/travelSeasons';

    async getTravelSeasons() {
        return this.get(this.uri);
    }

    async addTravelSeason(travelSeason: TravelSeason) {
        return this.post<TravelSeason>(this.uri, travelSeason);
    }

    async updateTravelSeason(travelSeason: TravelSeason) {
        return this.patch<TravelSeason>(this.uri + `/${travelSeason.id}`, travelSeason);
    }
}

And when I comment lines below:

export default class BaseDataSource extends RESTDataSource {
    baseURL = config.API_URL;

    // protected post<TResult = any>(path: string, body?: Body, init?): Promise<TResult> {
    //     return super.post(path, JSON.stringify(body), {
    //         headers: {
    //             "Content-Type": "application/json"
    //         },
    //         ...init
    //     });
    // }

    ...
}

I will get this:

{
  "data": {
    "addTravelSeason": null
  },
  "errors": [
    {
      "message": "Cannot convert object to primitive value",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "addTravelSeason"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "TypeError: Cannot convert object to primitive value",
            "    at String (<anonymous>)",
            "    at Request.Body (/usr/src/app/node_modules/node-fetch/lib/index.js:172:10)",
            "    at new Request (/usr/src/app/node_modules/node-fetch/lib/index.js:1169:8)",
            "    at DataSource.<anonymous> (/usr/src/app/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:140:29)",
            "    at Generator.next (<anonymous>)",
            "    at fulfilled (/usr/src/app/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:4:58)",
            "    at <anonymous>",
            "    at process._tickCallback (internal/process/next_tick.js:188:7)"
          ]
        }
      }
    }
  ]
}

My node version is: 8.11.3, all libraries are up to date.

Btw what about "Content-Type": "application/json" header? Is there a good way to add it to my datasource by default (in base class or sth - I guess some kind of adapter) ? Is my solution ok?

apollo-datasource-rest httpcache cacheOptions function missing await at line 111

path : packages/apollo-datasource-rest/src/HTTPCache.ts line 111
issue : missing await
current code :
if (typeof cacheOptions === 'function') {
cacheOptions = cacheOptions(response, request);
}

change required :
if (typeof cacheOptions === 'function') {
cacheOptions = await cacheOptions(response, request);
}

actual behavior : when we are sending cacheOptions as function and trying to read ttl value from response object (response.json()) , it is returning promise. and caching is not working with ttl.

expected behavior : when we override cacheOptions function and read ttl value from response object , it should wait for promise to resolve. so caching working properly with ttl.

NOTE : With this minor change it will work both way whether cacheOption function is returning promise or not.

This issue is in latest version and i think in all previous version.

reproduce :

this.post(url, body, {
headers ,
cacheOptions: async (response) => {
const res = await response.clone().json()
return { ttl: parseInt(res.expiryTimeInSeconds) }
}
});

[apollo-datasource-rest] Feature request: expose cache status to callers

Hey folks ๐Ÿ‘‹ We have an existing subclass of RESTDataSource that logs a variety of metrics for each call to fetch. We're trying to instrument our data sources to better understand how caching/memoization is used in production. However, RESTDataSource doesn't make it easy to figure out this information; the best we could do was manually querying the cache and memoizedResults to try to infer what's happening. However, in the end, we ended up forking RESTDataSource/HTTPCache to make cache status information first-class data in the return values from get/post/etc. We defined a new type, FetchResult that wraps the original response with cache metadata:

export interface FetchResult<TResult> {
  context: {
    cacheHit: boolean;
    memoized: boolean;
  };
  response: Promise<TResult>;
}

We then updated the get/post/etc. to return a FetchResult:

  protected async get<TResult = any>(
    path: string,
    params?: URLSearchParamsInit,
    init?: RequestInit
  ): Promise<FetchResult<TResult>> {
    return this.fetch<TResult>(
      Object.assign({ method: 'GET', path, params }, init)
    );
  }

Finally, we changed RESTDataSource#fetch and HTTPCache#fetch to return objects with that same context property. With this, we could update our subclass of RESTDataSource to automatically report whether particular requests were served by the cache or were memoized.

Here's our implementation in a Gist: https://gist.github.com/nwalters512/472b5fb7d4cc7d32c4cecaa69b21baf5. The important bits:

While this works, it's less than ideal to have to fork RESTDataSource and HTTPCache, since that introduces additional maintenance burden on our team. Ideally, this could be provided by the apollo-datasource-rest package itself. Does Apollo have any interest in adding this functionality? It doesn't necessarily need to use the same FetchResult interface we invented, but we'd appreciate anything that would give us more insight into how the cache is used.

RESTDataSource - body based cacheKeyFor POST request

I would like to use cache keys with request body for POST requests.

  protected cacheKeyFor(request: Request): string {
    return request.text(); // This type is Promise<string>
  }

Current cacheKeyFor interface in RESTDataSource provides a request object but it seems it doesn't contains simple body text.
Please help.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

circleci
.circleci/config.yml
  • node 5.2.0
  • secops 2.0.7
github-actions
.github/workflows/release-pr.yml
  • actions/checkout v4
  • actions/setup-node v4
  • changesets/action v1
npm
package.json
  • @apollo/utils.fetcher ^3.0.0
  • @apollo/utils.keyvaluecache ^3.1.0
  • @apollo/utils.logger ^3.0.0
  • @apollo/utils.withrequired ^3.0.0
  • @types/http-cache-semantics ^4.0.1
  • http-cache-semantics ^4.1.1
  • lodash.clonedeep ^4.5.0
  • lodash.isplainobject ^4.0.6
  • node-fetch ^2.6.7
  • @apollo/server 4.10.2
  • @changesets/changelog-github 0.5.0
  • @changesets/cli 2.27.1
  • @types/jest 29.5.12
  • @types/lodash.clonedeep 4.5.9
  • @types/lodash.isplainobject 4.0.9
  • @types/node 16.18.91
  • @typescript-eslint/eslint-plugin 6.21.0
  • @typescript-eslint/parser 6.21.0
  • cspell 8.6.0
  • eslint 8.57.0
  • form-data 4.0.0
  • graphql 16.8.1
  • jest 29.7.0
  • jest-junit 16.0.0
  • nock 13.5.4
  • prettier 3.2.5
  • ts-jest 29.1.2
  • ts-node 10.9.2
  • typescript 5.4.3
  • graphql ^16.5.0
  • node >=16.14
  • node 20.11.1
  • npm 10.5.0

  • Check this box to trigger a request for Renovate to run again on this repository

RESTDataSource delete method doesn't accept body param

https://github.com/apollographql/apollo-server/blob/master/packages/apollo-datasource-rest/src/RESTDataSource.ts#L180
this method doesn't have body param to pas to request. could you add it? there's no reason to not support it because delete request supports it:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE

I know that the use of it is not recommended, but it's it's not prohibited either. and we're dealing with API that is using this kind of thing.

also, fetch method is private. so if any of the provided methods doesn't suit our needs, we're blocked from using this class.
https://github.com/apollographql/apollo-server/blob/master/packages/apollo-datasource-rest/src/RESTDataSource.ts#L190
also other methods (like post, put or patch) doesn't accept params property. so I don't understand why you're limiting other methods only to body params.

Apollo version: 2

Typescript definition improvement: RESTDataSource class async functions

The RESTDataSource class automatically explicitly declares a default type of any https://github.com/apollographql/apollo-server/blob/master/packages/apollo-datasource-rest/src/RESTDataSource.ts#L47 for the async functions get, post, etc

We should force the user to declare the type. The library should not assume any.

As a workaround, I've been doing this, but it's very inconvenient because the code compiles and the type is simply inferred as unknown, which isn't immediately obvious to the consumer:

export class BaseDataSource extends RESTDataSource {
  constructor() {
    super();
  }

  protected async get<TResult>(...args: Parameters<RESTDataSource['get']>) {
    return super.get<TResult>(...args);
  }

  protected async post<TResult>(...args: Parameters<RESTDataSource['post']>) {
    return super.post<TResult>(...args);
  }

  protected async patch<TResult>(...args: Parameters<RESTDataSource['patch']>) {
    return super.patch<TResult>(...args);
  }

  protected async put<TResult>(...args: Parameters<RESTDataSource['put']>) {
    return super.put<TResult>(...args);
  }

  protected async delete<TResult>(...args: Parameters<RESTDataSource['delete']>) {
    return super.delete<TResult>(...args);
  }
}

[apollo-datasource-rest] Content-type: application/json not added when body is string

package

apollo-datasource-rest: 0.9.3

Expected behaviour

The content-type header, if not present, is added when a valid JSON is provided in the body.

Actual behaviour

The content-type header, if not present, is added only if the body contains an object, array or date.

affected code

A simple example (please pay no attention to the validity of the queries)

The point is to debug the provided affected code when the query
query{ hello bye }
is executed.

The hello query passes a string in the body and the bye one passes an object. They both use the datasource's put.

First request:
imagem

Second Request:
imagem

RESTDataSource delete method doesn't work using context header

// CODE BELOW DOESN'T WORK
await this.delete(`contacts/${contactId}`, undefined, { headers: this.context.req.headers });
await this.fetch({ method: 'DELETE', path: `contacts/${contactId}`, headers: this.context.req.headers });

// CODE BELOW WORKS
await this.delete(`contacts/${contactId}`);

Why I cannot set a custom header to do a delete operation? For post and patch actions works fine, but no for delete. Why is that so?

[apollo-datasource-rest] HTTPCache incorrectly caches 302 redirects

In package apollo-datasource-rest, the datasource is incorrectly storing a 302 redirect with the caching headers supplied by the final resource. Then when the request is checked, the original URL is assumed to be fresh and it does not check the original redirect again. This goes against the HTTP spec for 302 redirects (and caused a problem for me in my app)

I've created an integration spec that demonstrates the problem in my fork: https://github.com/gburgett/apollo-server/blob/rest-caching/packages/apollo-datasource-rest/src/__tests__/integration.test.ts

To reproduce:

  • Set up a REST API with a 302 redirect to some static resource with cache-control: public, max-age=31536000, immutable
  • Issue a GraphQL query that performs a GET request to this 302 redirect path
  • Change the 302 redirect to point to another resource
  • Issue the same GraphQL query again

At this point, the expected behavior is that the new resource is returned. The actual behavior is that the old resource is served from the HTTP cache.

Our current workaround is to set the TTL to 0 in the caching options for this data source.

[apollo-datasource-rest] Handling 404 errors

From my understanding of GraphQL data that is not available as part of a request should be returned as null, therefore allowing other parts of the query to return for the client to render as best.

If this is the case and your rest API returns 404 status codes however you'll need to write something like the following to capture the errors and return null;

      try {
        return await dataSources.listingApi.getListing(args.reference);
      } catch (err) {
        
        if(err.extensions.response.status === 404) {
          return null;
        }
        throw err;
      }

This works but isn't particularly elegant for what seems to be a common use case. Have I misunderstood how to deal with 404 errors in GraphQL or is this part of the rest datasource that could have better handling around it?

I'd be interested to know how other people are dealing with this in a more elegant manor or at least encapsulating the null return inside the class which extends RESTDataSource. From skimming the code this didn't look easy to handle without rewriting some of the protected methods.

[apollo-datasource-rest] (node:30845) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'fetch' of undefined

This bug report should include:

  • A short, but descriptive title. The title doesn't need "Apollo" in it.
  • The package name and version of Apollo showing the problem.
  • If applicable, the last version of Apollo where the problem did not occur.
  • The expected behavior.
  • The actual behavior.
  • A simple, runnable reproduction!
    Please make a GitHub repository that anyone can clone and run and see the
    problem. Other great ways to demonstrate a reproduction are using our
    CodeSandbox template (https://codesandbox.io/s/apollo-server), or re-mixing
    our Glitch template (https://glitch.com/~apollo-launchpad).
    -->

First off, you DO NOT explain how one can obtain info you demand above.
Hence I only add info I know how to obtain.

server/package.json

  "dependencies": {
    "apollo-datasource": "^0.1.3",
    "apollo-datasource-rest": "^0.1.5",
    "apollo-server": "2.6.1",
    "apollo-server-testing": "2.6.1",
    "graphql": "^14.2.1",
    "isemail": "^3.1.3",
    "nodemon": "^1.18.4",
    "sequelize": "^4.39.0",
    "sqlite3": "^4.0.3"
  },

apollo-datasource-rest throws error in GET requersts

The expected behavior.
const {RESTDataSource} = require('apollo-datasource-rest'); can make REST request without errors.

The actual behavior.

(node:30845) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'fetch' of undefined
    at LaunchAPI.<anonymous> (/Users/admin//apollo/fullstack-tutorial/start/server/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:148:63)
    at Generator.next (<anonymous>)
    at /Users/admin//apollo/fullstack-tutorial/start/server/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:7:71
    at new Promise (<anonymous>)
    at __awaiter (/Users/admin//apollo/fullstack-tutorial/start/server/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:3:12)
    at trace (/Users/admin//apollo/fullstack-tutorial/start/server/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:143:78)
    at LaunchAPI.<anonymous> (/Users/admin//apollo/fullstack-tutorial/start/server/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:186:24)
    at Generator.next (<anonymous>)
    at /Users/admin//apollo/fullstack-tutorial/start/server/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:7:71
    at new Promise (<anonymous>)
(node:30845) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
(node:30845) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

A simple, runnable reproduction!

  1. Follow your Tutorial
  2. Follow this step Hook up your data sources up to Connect a database section (don't implement it)
  3. Open index.js file and add the following lines:
const api = new LaunchAPI();
const launches = api.getAllLaunches(); // <== THIS THROWS

Why it doesn't work?
What is property 'fetch' of undefined?

Related issue
apollographql/apollo-server#3429

[apollo-datasource-rest] - Support fetch arraybuffer and blob response types

I need to fetch raw image data from a service and then return a base64 encoded version of that data to the front end. After testing, I discovered that the stock node-fetch implementation is mangling the data without the appropriate response type. There doesn't appear to currently be a way with RESTDatasource to resolve the different types that fetch supports (arraybuffer, blob, text, json, etc).

Is this something that can be added in?

Rest datasource caches data even when the cache control header is no-cache and also when explictly configured as no-cache or no-store

  • Package name and version
"apollo-datasource-rest": "^0.13.0",
    "apollo-server": "^2.24.1",
    "graphql": "^15.5.0"
  • Expected behavior

    • Expected it to not cache the api response when the response has header cache-control: no-cache
    • Expected it to not cache the api response when explicitly configured to do so. Example
      this.get(`https://randomuser.me/api/`, undefined, { cache: "no-cache", });
      this.get(`https://randomuser.me/api/`, undefined, { cache: "no-store", });
      this.get(`https://randomuser.me/api/`, undefined, { cache: "no-cache", cacheOptions: { ttl: 0 }, });
  • Actual behavior
    - The api response is cached in either of the cases.
    - I have demonstrated the caching behavior with the time it takes to fulfill the subsequent request in the exaple repo given below
    - The actual time may depend on the connection speed and location. But the overall pattern remains the same
    - Please also refer to the screenshots

  • Repo for reproduction
    Refer to the instructions given in the readme https://github.com/shreyas1496/apollo-cache-issue

Expected logs in console
Screenshot from 2021-05-25 11-13-49

Actual logs in console
Screenshot from 2021-05-25 11-13-43

RESTDataSource serializes undefined values as "undefined"

I have a data source like this:

class MyApi extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://my-api.example.com/';
  }

  async getUser(query) {
    return this.get('/user', query);
  }
}

This is my resolver:

const userResolver = (_, __, {dataSource: {myApi}}) => {
  const query = {
    userId: "1",
    optionalParam: undefined,
  };
  return myApi.getUser(query);
}

This leads to this HTTP request:

GET /myApi/user?userId=1&optionalParam=undefined

while I think it would be more logical if this was fetched:

GET /myApi/user?userId=1

I think these lines implement the behavior:

https://github.com/apollographql/apollo-server/blob/d3882350fb768bde2d599343c257dc71400cb360/packages/apollo-datasource-rest/src/RESTDataSource.ts#L226-L229

Cached REST-Response does not contain URL

I use apollo-datasource-rest to cache responses from remote REST-APIs. Responses that have a 404 status get cached and thus read from cached on subsequent calls to that same endpoint. This is fine, until the error for the request gets created. The first request - the one that actually hit the remote and not the cache - creates an error like this:

      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "response": {
          "url": "http://localhost:4000/someResource",
          "status": 404,
          "statusText": "Not Found",
          "body": "Not found"
        },
        "exception": {
          "stacktrace": [
            "Error: 404: Not Found",
            "    at ExampleDataSource.<anonymous> (/sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:84:25)",
            "    at Generator.next (<anonymous>)",
            "    at /sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:8:71",
            "    at new Promise (<anonymous>)",
            "    at __awaiter (/sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:4:12)",
            "    at ExampleDataSource.errorFromResponse (/sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:74:16)",
            "    at ExampleDataSource.<anonymous> (/sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:52:34)",
            "    at Generator.next (<anonymous>)",
            "    at /sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:8:71",
            "    at new Promise (<anonymous>)"
          ]
        }
      }

The important bit is the url-field telling us which exact request failed. The actual issue lies in the second request - the one that hits the cache -, which creates the following error:

      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "response": {
          "status": 404,
          "statusText": "Not Found",
          "body": "Not found"
        },
        "exception": {
          "stacktrace": [
            "Error: 404: Not Found",
            "    at ExampleDataSource.<anonymous> (/sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:84:25)",
            "    at Generator.next (<anonymous>)",
            "    at /sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:8:71",
            "    at new Promise (<anonymous>)",
            "    at __awaiter (/sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:4:12)",
            "    at ExampleDataSource.errorFromResponse (/sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:74:16)",
            "    at ExampleDataSource.<anonymous> (/sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:52:34)",
            "    at Generator.next (<anonymous>)",
            "    at /sandbox/node_modules/apollo-datasource-rest/dist/RESTDataSource.js:8:71",
            "    at new Promise (<anonymous>)"
          ]
        }
      }

As you can see, the url-field is missing. Interestingly enough, this only happens if i set a ttl in the RequestInit object I pass to the datasource. I prepared a small reproduction for this issue: https://codesandbox.io/s/little-microservice-hm0sv

I analyzed the source and believe the line causing the issue is this one: https://github.com/apollographql/apollo-server/blob/c177acd7959aaf516f8ac814f1a6bb2e6d3ed2e6/packages/apollo-datasource-rest/src/HTTPCache.ts#L60
Before spending too much time looking for possible solutions, I wanted to make sure that this is in fact an issue and not something you actually intended because of some piece of information that I'm currently missing.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.