Repository: conan-io/conan
Pull request: [feat] Send HTTP GET header with settings when looking for a 'package_reference'
Changelog: Feature: Add headers with settings and options to HTTP GET requests when searching for packages.
Docs: Omit

closes https://github.com/conan-io/conan/issues/7870

Conan sends a request header `Conan-PkgID-Settings` when looking for a `pref` in the remotes:
 * if using revisions, it is sent when looking for the latest package revision, URL like:
   ```
   v2/conans/<reference>/revisions/<rrev>/packages/<package_id>/latest
   ```

 * if not using revisions, it is in the downloads_url:
   ```
   v1/conans/<reference>/packages/<package_id>/download_urls
   ```

These are the headers, do we want this information in a single header? do we want to use different names?

```
Conan-PkgID-Settings
Conan-PkgID-Options
```

---

#REVISIONS: 1

Problem statement:
[feature] Identify configuration in remote requests when querying for a packageID
When Conan checks if a package for a given configuration is available in a remote, it sends just the _package ID_. It would be very useful to send the information used to compute that hash:
 * We can send _all_ the configuration: _package ID_ computation is reproducible (needed? desired?)
 * We can send only the information taken into account to compute the _package ID_ (~`conaninfo.txt`)

Hints from issue discussion:
I've been having a look at it and it can be very challenging to pass the information about the configuration to the place where the calls to the remote are being done. I would say we need first to store the actual configuration for a package (settings, options, requirements,...) in the `node` object to have it available here https://github.com/conan-io/conan/blob/7f03c1d340eedcf9772722361a42dfb4ba2fc81e/conans/client/graph/graph_binaries.py#L100

Then we can deal with whatever is needed on the RemoteManager side.
> I've been having a look at it and it can be very challenging to pass the information about the configuration to the place where the calls to the remote are being done. I would say we need first to store the actual configuration for a package (settings, options, requirements,...) in the node object to have it available here

That would be accessible in ``node.conanfile.info`` object at that point.

Commit message: handle info variable with care

Patch to review:
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index 798fbe3ef35..69da5ae8dad 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -1133,7 +1133,8 @@ def get_path(self, reference, package_id=None, path=None, remote_name=None):
             if package_id:
                 pref = PackageReference(ref, package_id)
                 if self.app.config.revisions_enabled and not pref.revision:
-                    pref = self.app.remote_manager.get_latest_package_revision(pref, remote)
+                    pref = self.app.remote_manager.get_latest_package_revision(pref, remote,
+                                                                               headers=None)
                 return self.app.remote_manager.get_package_path(pref, path, remote), path
             else:
                 return self.app.remote_manager.get_recipe_path(ref, path, remote), path
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index 033067b313a..ee81dd4aab6 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -93,11 +93,14 @@ def _evaluate_cache_pkg(self, node, package_layout, pref, metadata, remote, remo
             node.prev = metadata.packages[pref.id].revision
             assert node.prev, "PREV for %s is None: %s" % (str(pref), metadata.dumps())
 
+    def _get_package_info(self, node, pref, remote):
+        return self._remote_manager.get_package_info(pref, remote, info=node.conanfile.info)
+
     def _evaluate_remote_pkg(self, node, pref, remote, remotes):
         remote_info = None
         if remote:
             try:
-                remote_info, pref = self._remote_manager.get_package_info(pref, remote)
+                remote_info, pref = self._get_package_info(node, pref, remote)
             except NotFoundException:
                 pass
             except Exception:
@@ -107,9 +110,9 @@ def _evaluate_remote_pkg(self, node, pref, remote, remotes):
         # If the "remote" came from the registry but the user didn't specified the -r, with
         # revisions iterate all remotes
         if not remote or (not remote_info and self._cache.config.revisions_enabled):
-            for r in remotes.values():
+            for r in remotes.values():  # FIXME: Here we hit the same remote we did before
                 try:
-                    remote_info, pref = self._remote_manager.get_package_info(pref, r)
+                    remote_info, pref = self._get_package_info(node, pref, r)
                 except NotFoundException:
                     pass
                 else:
@@ -236,7 +239,7 @@ def _process_node(self, node, pref, build_mode, update, remotes):
         if build_mode.outdated:
             if node.binary in (BINARY_CACHE, BINARY_DOWNLOAD, BINARY_UPDATE):
                 if node.binary == BINARY_UPDATE:
-                    info, pref = self._remote_manager.get_package_info(pref, remote)
+                    info, pref = self._get_package_info(node, pref, remote)
                     recipe_hash = info.recipe_hash
                 elif node.binary == BINARY_CACHE:
                     package_folder = package_layout.package(pref)
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index 6df71cd4dbc..8c72ed459a8 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -20,6 +20,27 @@
                                 log_recipe_download, log_recipe_sources_download,
                                 log_uncompressed_file)
 
+CONAN_REQUEST_HEADER_SETTINGS = 'Conan-PkgID-Settings'
+CONAN_REQUEST_HEADER_OPTIONS = 'Conan-PkgID-Options'
+
+
+def _headers_for_info(info):
+    if not info:
+        return None
+
+    r = {}
+    settings = info.full_settings.as_list()
+    if settings:
+        settings = ['{}={}'.format(*it) for it in settings]
+        r.update({CONAN_REQUEST_HEADER_SETTINGS: ';'.join(settings)})
+
+    options = info.options.as_list()
+    if options:
+        options = filter(lambda u: u[0] in ['shared', 'fPIC', 'header_only'], options)
+        options = ['{}={}'.format(*it) for it in options]
+        r.update({CONAN_REQUEST_HEADER_OPTIONS: ';'.join(options)})
+    return r
+
 
 class RemoteManager(object):
     """ Will handle the remotes to get recipes, packages etc """
@@ -38,7 +59,7 @@ def get_recipe_snapshot(self, ref, remote):
         return self._call_remote(remote, "get_recipe_snapshot", ref)
 
     def get_package_snapshot(self, pref, remote):
-        assert pref.ref.revision, "upload_package requires RREV"
+        assert pref.ref.revision, "get_package_snapshot requires RREV"
         assert pref.revision, "get_package_snapshot requires PREV"
         return self._call_remote(remote, "get_package_snapshot", pref)
 
@@ -58,14 +79,16 @@ def get_recipe_manifest(self, ref, remote):
         return self._call_remote(remote, "get_recipe_manifest", ref), ref
 
     def get_package_manifest(self, pref, remote):
-        pref = self._resolve_latest_pref(pref, remote)
+        pref = self._resolve_latest_pref(pref, remote, headers=None)
         return self._call_remote(remote, "get_package_manifest", pref), pref
 
-    def get_package_info(self, pref, remote):
+    def get_package_info(self, pref, remote, info=None):
         """ Read a package ConanInfo from remote
         """
-        pref = self._resolve_latest_pref(pref, remote)
-        return self._call_remote(remote, "get_package_info", pref), pref
+        headers = _headers_for_info(info)
+        pref = self._resolve_latest_pref(pref, remote, headers=headers)
+        # FIXME Conan 2.0: With revisions, it is not needed to pass headers to this second function
+        return self._call_remote(remote, "get_package_info", pref, headers=headers), pref
 
     def get_recipe(self, ref, remote):
         """
@@ -140,16 +163,18 @@ def get_package(self, conanfile, pref, layout, remote, output, recorder):
         output.info("Retrieving package %s from remote '%s' " % (pref.id, remote.name))
         layout.package_remove(pref)  # Remove first the destination folder
         with layout.set_dirty_context_manager(pref):
-            self._get_package(layout, pref, remote, output, recorder)
+            info = getattr(conanfile, 'info', None)
+            self._get_package(layout, pref, remote, output, recorder, info=info)
 
         self._hook_manager.execute("post_download_package", conanfile_path=conanfile_path,
                                    reference=pref.ref, package_id=pref.id, remote=remote,
                                    conanfile=conanfile)
 
-    def _get_package(self, layout, pref, remote, output, recorder):
+    def _get_package(self, layout, pref, remote, output, recorder, info):
         t1 = time.time()
         try:
-            pref = self._resolve_latest_pref(pref, remote)
+            headers = _headers_for_info(info)
+            pref = self._resolve_latest_pref(pref, remote, headers=headers)
             snapshot = self._call_remote(remote, "get_package_snapshot", pref)
             if not is_package_snapshot_complete(snapshot):
                 raise PackageNotFoundException(pref)
@@ -230,8 +255,8 @@ def get_latest_recipe_revision(self, ref, remote):
         revision = self._call_remote(remote, "get_latest_recipe_revision", ref)
         return revision
 
-    def get_latest_package_revision(self, pref, remote):
-        revision = self._call_remote(remote, "get_latest_package_revision", pref)
+    def get_latest_package_revision(self, pref, remote, headers):
+        revision = self._call_remote(remote, "get_latest_package_revision", pref, headers=headers)
         return revision
 
     def _resolve_latest_ref(self, ref, remote):
@@ -242,16 +267,16 @@ def _resolve_latest_ref(self, ref, remote):
                 ref = ref.copy_with_rev(DEFAULT_REVISION_V1)
         return ref
 
-    def _resolve_latest_pref(self, pref, remote):
+    def _resolve_latest_pref(self, pref, remote, headers):
         if pref.revision is None:
             try:
-                pref = self.get_latest_package_revision(pref, remote)
+                pref = self.get_latest_package_revision(pref, remote, headers=headers)
             except NoRestV2Available:
                 pref = pref.copy_with_revs(pref.ref.revision, DEFAULT_REVISION_V1)
         return pref
 
     def _call_remote(self, remote, method, *args, **kwargs):
-        assert(isinstance(remote, Remote))
+        assert (isinstance(remote, Remote))
         try:
             return self._auth_manager.call_rest_api_method(remote, method, *args, **kwargs)
         except ConnectionError as exc:
@@ -294,7 +319,7 @@ def uncompress_file(src_path, dest_folder, output):
     t1 = time.time()
     try:
         with progress_bar.open_binary(src_path, output, "Decompressing %s" % os.path.basename(
-                src_path)) as file_handler:
+            src_path)) as file_handler:
             tar_extract(file_handler, dest_folder)
     except Exception as e:
         error_msg = "Error while downloading/extracting files to %s\n%s\n" % (dest_folder, str(e))
diff --git a/conans/client/rest/rest_client.py b/conans/client/rest/rest_client.py
index b3b6dc7ef29..4ee532047a7 100644
--- a/conans/client/rest/rest_client.py
+++ b/conans/client/rest/rest_client.py
@@ -79,8 +79,8 @@ def get_recipe_manifest(self, ref):
     def get_package_manifest(self, pref):
         return self._get_api().get_package_manifest(pref)
 
-    def get_package_info(self, pref):
-        return self._get_api().get_package_info(pref)
+    def get_package_info(self, pref, headers):
+        return self._get_api().get_package_info(pref, headers=headers)
 
     def get_recipe(self, ref, dest_folder):
         return self._get_api().get_recipe(ref, dest_folder)
@@ -161,5 +161,5 @@ def get_package_revisions(self, pref):
     def get_latest_recipe_revision(self, ref):
         return self._get_api().get_latest_recipe_revision(ref)
 
-    def get_latest_package_revision(self, pref):
-        return self._get_api().get_latest_package_revision(pref)
+    def get_latest_package_revision(self, pref, headers):
+        return self._get_api().get_latest_package_revision(pref, headers=headers)
diff --git a/conans/client/rest/rest_client_common.py b/conans/client/rest/rest_client_common.py
index 769759e8050..18405e2cdf4 100644
--- a/conans/client/rest/rest_client_common.py
+++ b/conans/client/rest/rest_client_common.py
@@ -13,6 +13,7 @@
 
 class JWTAuth(AuthBase):
     """Attaches JWT Authentication to the given Request object."""
+
     def __init__(self, token):
         self.token = token
 
@@ -42,6 +43,7 @@ def handle_return_deserializer(deserializer=None):
     Map exceptions and http return codes and deserialize if needed.
 
     deserializer: Function for deserialize values"""
+
     def handle_return(method):
         def inner(*argc, **argv):
             ret = method(*argc, **argv)
@@ -50,7 +52,9 @@ def inner(*argc, **argv):
                 text = ret.text if ret.status_code != 404 else "404 Not found"
                 raise get_exception_from_error(ret.status_code)(text)
             return deserializer(ret.content) if deserializer else decode_text(ret.content)
+
         return inner
+
     return handle_return
 
 
@@ -169,19 +173,20 @@ def server_capabilities(self, user=None, password=None):
 
         return [cap.strip() for cap in server_capabilities.split(",") if cap]
 
-    def get_json(self, url, data=None):
-        headers = self.custom_headers
+    def get_json(self, url, data=None, headers=None):
+        req_headers = self.custom_headers.copy()
+        req_headers.update(headers or {})
         if data:  # POST request
-            headers.update({'Content-type': 'application/json',
-                            'Accept': 'application/json'})
+            req_headers.update({'Content-type': 'application/json',
+                                'Accept': 'application/json'})
             logger.debug("REST: post: %s" % url)
-            response = self.requester.post(url, auth=self.auth, headers=headers,
+            response = self.requester.post(url, auth=self.auth, headers=req_headers,
                                            verify=self.verify_ssl,
                                            stream=True,
                                            data=json.dumps(data))
         else:
             logger.debug("REST: get: %s" % url)
-            response = self.requester.get(url, auth=self.auth, headers=headers,
+            response = self.requester.get(url, auth=self.auth, headers=req_headers,
                                           verify=self.verify_ssl,
                                           stream=True)
 
@@ -244,4 +249,3 @@ def search_packages(self, ref, query):
         url = self.router.search_packages(ref, query)
         package_infos = self.get_json(url)
         return package_infos
-
diff --git a/conans/client/rest/rest_client_v1.py b/conans/client/rest/rest_client_v1.py
index 96fdbd43c84..ed6e34a384f 100644
--- a/conans/client/rest/rest_client_v1.py
+++ b/conans/client/rest/rest_client_v1.py
@@ -106,11 +106,11 @@ def get_package_manifest(self, pref):
             logger.error(traceback.format_exc())
             raise ConanException(msg)
 
-    def get_package_info(self, pref):
+    def get_package_info(self, pref, headers):
         """Gets a ConanInfo file from a package"""
         pref = pref.copy_with_revs(None, None)
         url = self.router.package_download_urls(pref)
-        urls = self._get_file_to_url_dict(url)
+        urls = self._get_file_to_url_dict(url, headers=headers)
         if not urls:
             raise PackageNotFoundException(pref)
 
@@ -124,10 +124,10 @@ def get_package_info(self, pref):
         contents = {key: decode_text(value) for key, value in dict(contents).items()}
         return ConanInfo.loads(contents[CONANINFO])
 
-    def _get_file_to_url_dict(self, url, data=None):
+    def _get_file_to_url_dict(self, url, data=None, headers=None):
         """Call to url and decode the json returning a dict of {filepath: url} dict
         converting the url to a complete url when needed"""
-        urls = self.get_json(url, data=data)
+        urls = self.get_json(url, data=data, headers=headers)
         return {filepath: complete_url(self.remote_url, url) for filepath, url in urls.items()}
 
     def _upload_recipe(self, ref, files_to_upload, retry, retry_wait):
@@ -340,7 +340,7 @@ def get_package_revisions(self, pref):
     def get_latest_recipe_revision(self, ref):
         raise NoRestV2Available("The remote doesn't support revisions")
 
-    def get_latest_package_revision(self, pref):
+    def get_latest_package_revision(self, pref, headers):
         raise NoRestV2Available("The remote doesn't support revisions")
 
     def _post_json(self, url, payload):
diff --git a/conans/client/rest/rest_client_v2.py b/conans/client/rest/rest_client_v2.py
index dd53f7b209f..8f49198e2ef 100644
--- a/conans/client/rest/rest_client_v2.py
+++ b/conans/client/rest/rest_client_v2.py
@@ -39,12 +39,12 @@ def _get_file_list_json(self, url):
         data["files"] = list(data["files"].keys())
         return data
 
-    def _get_remote_file_contents(self, url, use_cache):
+    def _get_remote_file_contents(self, url, use_cache, headers=None):
         # We don't want traces in output of these downloads, they are ugly in output
         downloader = FileDownloader(self.requester, None, self.verify_ssl, self._config)
         if use_cache and self._config.download_cache:
             downloader = CachedFileDownloader(self._config.download_cache, downloader)
-        contents = downloader.download(url, auth=self.auth)
+        contents = downloader.download(url, auth=self.auth, headers=headers)
         return contents
 
     def _get_snapshot(self, url):
@@ -77,10 +77,10 @@ def get_package_manifest(self, pref):
             logger.error(traceback.format_exc())
             raise ConanException(msg)
 
-    def get_package_info(self, pref):
+    def get_package_info(self, pref, headers):
         url = self.router.package_info(pref)
         cache = (pref.revision != DEFAULT_REVISION_V1)
-        content = self._get_remote_file_contents(url, use_cache=cache)
+        content = self._get_remote_file_contents(url, use_cache=cache, headers=headers)
         return ConanInfo.loads(decode_text(content))
 
     def get_recipe(self, ref, dest_folder):
@@ -329,9 +329,9 @@ def get_latest_recipe_revision(self, ref):
         # Ignored data["time"]
         return ref.copy_with_rev(rev)
 
-    def get_latest_package_revision(self, pref):
+    def get_latest_package_revision(self, pref, headers):
         url = self.router.package_latest(pref)
-        data = self.get_json(url)
+        data = self.get_json(url, headers=headers)
         prev = data["revision"]
         # Ignored data["time"]
         return pref.copy_with_revs(pref.ref.revision, prev)
diff --git a/conans/test/functional/remote/rest_api_test.py b/conans/test/functional/remote/rest_api_test.py
index 3fdd01e09b0..fd09f407a38 100644
--- a/conans/test/functional/remote/rest_api_test.py
+++ b/conans/test/functional/remote/rest_api_test.py
@@ -159,7 +159,7 @@ def test_get_package_info(self):
         self._upload_package(pref, {CONANINFO: conan_info})
 
         # Get the package info
-        info = self.api.get_package_info(pref)
+        info = self.api.get_package_info(pref, headers=None)
         self.assertIsInstance(info, ConanInfo)
         self.assertEqual(info, ConanInfo.loads(conan_info))
 
diff --git a/conans/test/functional/remote/test_request_headers.py b/conans/test/functional/remote/test_request_headers.py
new file mode 100644
index 00000000000..e1cedf3b5ed
--- /dev/null
+++ b/conans/test/functional/remote/test_request_headers.py
@@ -0,0 +1,160 @@
+import textwrap
+import unittest
+
+from parameterized.parameterized import parameterized_class
+
+from conans.client.remote_manager import CONAN_REQUEST_HEADER_SETTINGS, CONAN_REQUEST_HEADER_OPTIONS
+from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient, TestServer, TestRequester
+
+
+class RequesterClass(TestRequester):
+    requests = None
+
+    def __init__(self, *args, **kwargs):
+        self.requests = []
+        super(RequesterClass, self).__init__(*args, **kwargs)
+
+    def get(self, url, headers=None, **kwargs):
+        self.requests.append((url, headers))
+        return super(RequesterClass, self).get(url, headers=headers, **kwargs)
+
+
+@parameterized_class([{"revs_enabled": True}, {"revs_enabled": False}, ])
+class RequestHeadersTestCase(unittest.TestCase):
+    """ Conan adds a header with the settings used to compute the package ID """
+
+    profile = textwrap.dedent("""
+        [settings]
+        os=Macos
+        arch=x86_64
+        compiler=apple-clang
+        compiler.version=11.0
+        compiler.libcxx=libc++
+        build_type=Release
+    """)
+
+    conanfile = GenConanfile().with_settings('os', 'arch', 'compiler') \
+        .with_option('opt1', [True, False]) \
+        .with_option('shared', [True, False]) \
+        .with_default_option('opt1', True) \
+        .with_default_option('shared', False)
+
+    def setUp(self):
+        test_server = TestServer(users={"user": "mypass"})
+        self.servers = {"default": test_server}
+        t = TestClient(servers=self.servers, users={"default": [("user", "mypass")]})
+        t.save({'conanfile.py': self.conanfile,
+                'profile': self.profile})
+        t.run('create conanfile.py name/version@user/channel --profile:host=profile')
+        t.run('upload name/version@user/channel --all')
+
+    def _get_header(self, requester, header_name):
+        hits = sum([header_name in headers for _, headers in requester.requests])
+        self.assertEquals(hits, 2 if self.revs_enabled else 1)
+        for url, headers in requester.requests:
+            if header_name in headers:
+                if self.revs_enabled:
+                    self.assertTrue(url.endswith('/latest'), msg=url)
+                else:
+                    self.assertTrue(url.endswith('/download_urls'), msg=url)
+                return headers.get(header_name)
+
+    def _assert_settings_headers(self, settings_header, compiler_version='11.0'):
+        # It takes only the values that are relevant to the recipe
+        self.assertListEqual(
+            sorted(['os', 'arch', 'compiler', 'compiler.version', 'compiler.libcxx']),
+            sorted([it.split('=', 1)[0] for it in settings_header.split(';')]))
+        self.assertIn('os=Macos', settings_header)
+        self.assertIn('arch=x86_64', settings_header)
+        self.assertIn('compiler=apple-clang', settings_header)
+        self.assertIn('compiler.libcxx=libc++', settings_header)
+        self.assertIn('compiler.version={}'.format(compiler_version), settings_header)
+        self.assertNotIn('build_type', settings_header)
+
+    def _assert_options_headers(self, options_header, shared_value='False'):
+        self.assertListEqual(['shared'], [it.split('=', 1)[0] for it in options_header.split(';')])
+        self.assertIn('shared={}'.format(shared_value), options_header)
+
+    def _get_test_client(self):
+        t = TestClient(requester_class=RequesterClass, servers=self.servers,
+                       users={"default": [("user", "mypass")]})
+        t.run('config set general.revisions_enabled={}'.format('1' if self.revs_enabled else '0'))
+        return t
+
+    def test_install_recipe_mismatch(self):
+        t = self._get_test_client()
+        t.save({'profile': self.profile})
+        t.run('install failing/version@user/channel --profile=profile', assert_error=True)
+        self.assertFalse(any([CONAN_REQUEST_HEADER_SETTINGS in headers for _, headers in
+                              t.api.http_requester.requests]))
+        self.assertFalse(any([CONAN_REQUEST_HEADER_OPTIONS in headers for _, headers in
+                              t.api.http_requester.requests]))
+
+    def test_install_package_match(self):
+        t = self._get_test_client()
+        t.save({'profile': self.profile})
+
+        # Package match
+        t.run('install name/version@user/channel --profile=profile')
+        settings_header = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_SETTINGS)
+        self._assert_settings_headers(settings_header)
+        options_headers = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_OPTIONS)
+        self._assert_options_headers(options_headers)
+
+        # Package mismatch (settings)
+        t.run('install name/version@user/channel --profile=profile -s compiler.version=12.0',
+              assert_error=True)
+        settings_header = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_SETTINGS)
+        self._assert_settings_headers(settings_header, compiler_version='12.0')
+
+        # Package mismatch (options)
+        t.run('install name/version@user/channel --profile=profile -o shared=True',
+              assert_error=True)
+        options_headers = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_OPTIONS)
+        self._assert_options_headers(options_headers, shared_value='True')
+
+    def test_info_package_match(self):
+        t = self._get_test_client()
+        t.save({'profile': self.profile})
+
+        # Package match
+        t.run('info name/version@user/channel --profile=profile')
+        settings_header = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_SETTINGS)
+        self._assert_settings_headers(settings_header)
+        options_headers = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_OPTIONS)
+        self._assert_options_headers(options_headers)
+
+        # Package mismatch (settings)
+        t.run('info name/version@user/channel --profile=profile -s compiler.version=12.0')
+        settings_header = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_SETTINGS)
+        self._assert_settings_headers(settings_header, compiler_version='12.0')
+
+        # Package mismatch (options)
+        t.run('install name/version@user/channel --profile=profile -o shared=True',
+              assert_error=True)
+        options_headers = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_OPTIONS)
+        self._assert_options_headers(options_headers, shared_value='True')
+
+    def test_install_as_requirement(self):
+        t = self._get_test_client()
+        t.save({'conanfile.py': GenConanfile().with_requires('name/version@user/channel'),
+                'profile': self.profile})
+
+        # Requirement is found
+        t.run('install . consumer/version@ --profile=profile')
+        settings_header = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_SETTINGS)
+        self._assert_settings_headers(settings_header)
+        options_headers = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_OPTIONS)
+        self._assert_options_headers(options_headers)
+
+        # Requirement is not found (settings)
+        t.run('install . consumer/version@ --profile=profile -s compiler.version=12.0',
+              assert_error=True)
+        settings_header = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_SETTINGS)
+        self._assert_settings_headers(settings_header, compiler_version='12.0')
+
+        # Requirement is not found (options)
+        t.run('install . consumer/version@ --profile=profile -o name:shared=True', assert_error=True)
+        options_headers = self._get_header(t.api.http_requester, CONAN_REQUEST_HEADER_OPTIONS)
+        self._assert_options_headers(options_headers, shared_value='True')


Code review comments:
diff_hunk: @@ -1133,7 +1133,8 @@ def get_path(self, reference, package_id=None, path=None, remote_name=None):
             if package_id:
                 pref = PackageReference(ref, package_id)
                 if self.app.config.revisions_enabled and not pref.revision:
-                    pref = self.app.remote_manager.get_latest_package_revision(pref, remote)
+                    pref = self.app.remote_manager.get_latest_package_revision(pref, remote,
path: conans/client/conan_api.py
text: @user1:
Maybe define a default=None for headers arg?