循序渐进 · 教学 · 函数(十六)

从函数里调用外部 API

函数可以调外部系统,但必须先在平台上配置数据源和凭据。这篇讲配置外部 API 访问、在函数里使用、OAuth 2.0,以及常见报错排查。

全部目录 ← 上一篇 从函数里调用外部 API 下一篇 →
本文来源 · Source 内容整理自 Palantir Foundry 官方文档:
https://www.palantir.com/docs/foundry/functions/api-calls/
原始标题:Language-agnostic features > Make API calls from functions

先记住这几条

① 必须先配置外部源
不能直接在代码里写死地址和密码,要先在平台登记 source。
② 凭据由平台托管
函数里拿到的是预配置好的 client,不接触明文密钥。
③ OAuth 2.0 有专门流程
需要 outbound application 配合,有几种用法可选。
④ 常见报错是代理认证
HTTP 407 是最高频的报错,原因通常是出口代理没配好。
0

写在前面

可以从 TypeScript v1、TypeScript v2 与 Python 函数向外部源发起 API 调用,但这需要额外的配置。下面的内容与外部源的使用一起详细说明了这些配置。

:::callout{theme="neutral" title="Source aliases"}

对于 TypeScript v2 与 Python 函数,我们建议通过源别名(source aliases)引用源。源别名是一个可移植的、具名的引用,你可以将它作为源标识符,替代具体的源。当你的函数通过 Marketplace 产品 分发时,别名可以按环境重新映射到不同的源,从而保持你的函数代码可移植。TypeScript v1 函数使用生成的源符号,不支持别名。

1

配置外部 API 访问

配置外部 API 访问

要点:第一步永远是配置,代码之前先把通路打通。

默认情况下,函数不允许调用外部 API。要启用从函数调用外部系统,你必须在 Data Connection配置一个源,以允许 Foundry 与该外部系统连接。

为了让函数安全地连接到你源的外部系统,你的源必须配置为启用导出,并允许将源导入 Code Repositories。这两项都可以通过在 Data Connection 中导航到该源并打开 Connection settings(连接设置) 部分来配置。

对于 TypeScript v1 函数,源的 API 名称(在 Connection settings 下的 Code import configuration 标签页配置)是你在代码中引用的标识符。

请务必在你的源中完整配置证书链。<br><br> Webhook 与函数的运行时环境并不完全相同。<br><br> 有时,webhook 能正常工作,而从函数发起的 API 调用却可能遇到 UNABLE_TO_GET_ISSUER_CERT 错误。<br><br> 请参阅我们关于源终端中 openssl 命令的文档来验证证书。

2

在函数里使用外部源

在函数里使用外部源

要点:配置好之后,代码里如何引用。

要从函数发起 API 调用,你必须首先使用资源导入侧边栏将你的源导入一个函数仓库。对于 TypeScript v2 与 Python 函数,我们随后建议创建一个源别名,并使用其别名键作为源标识符。TypeScript v1 函数直接引用导入的源。然后你必须声明你的函数使用了该源,如下面示例所示。

示例如下:

import { ExternalSystems } from "@foundry/functions-api";
import { MySource } from "@foundry/external-systems/sources";

export class MyExternalFunctions {
    @ExternalSystems({ sources: [MySource] })
    @Function()
    public async myExternalFunction(): Promise<string> {
        const { url } = MySource.getHttpsConnection();
        const response = await MySource.fetch(url);

        return response.text();
    }
}
import { getSource, getHttpsConnection, getFetch } from "@palantir/functions-sources";

export const config = {
    sources: ["mySourceAlias"]
}

async function MyExternalFunction(): Promise<string> {
    const source = await getSource("mySourceAlias");
    const { url } = getHttpsConnection(source);
    const fetch = await getFetch(source);

    const response = await fetch(url);

    return response.text();
}
from functions.api import function
from functions.sources import get_source


@function(sources=["mySourceAlias"])
def my_external_function() -> str:
    source = get_source("mySourceAlias")
    url = source.get_https_connection().url
    client = source.get_https_connection().get_client()
    response = client.get(url)
    return response.text

你可以在实时预览中测试你的函数,并在发布后使用它发起外部调用。

在 serverless 执行或实时预览中,尚不支持使用第三方客户端,除非覆写 fetch 函数或 HTTP agent。为确保你的 API 调用在所有环境中都能正常工作,你必须使用相关的库方法来发起带正确配置的请求。对外部源或内部 Foundry URL 的直接 API 调用,不能保证在所有环境中都有效。

3

访问源属性与凭据

访问源属性与凭据

要点:怎么拿到地址和凭据(注意:不写死在代码里)。

你可以访问每种函数类型对应库所提供的源属性。

下面的示例展示了如何获取上面例子中源的 base URL。

const { url } = MySource.getHttpsConnection();
const { url } = getHttpsConnection(source);
url = get_source("mySourceAlias").get_https_connection().url

你也可以使用以下语法访问源上存储的额外密钥或凭证:

const secret = MySource.getSecret("MySecret");
const secret = source.secrets["MySecret"];
secret = get_source("mySourceAlias").get_secret("MySecret")
4

使用预配置客户端

使用预配置客户端

要点:直接用平台准备好的 client,省去认证细节。

对于提供 REST API 的源,源对象允许你获取一个客户端。该客户端会预配置源上指定的服务端证书与客户端证书。它还会包含额外的代理配置,允许从函数执行所在环境向外出口(egress)。如果可能,你应当始终使用这个客户端,以保证你的函数能从所有环境出口到该源。

const fetch = MySource.fetch;
const fetch = await getFetch(source);
client = source.get_https_connection().get_client()

另外,你也可以使用自己的客户端或发起外部请求的第三方库,并用源对象获取属性与凭证

TypeScript v2 函数提供了一个预配置的 HTTP agent,作为接受自定义 HTTP agent 的第三方库的额外集成点。

下面的示例演示了获取该 agent 并将其用于 axios ↗

import { getHttpAgent, getHttpsConnection } from "@palantir/functions-sources";
import axios from 'axios';

const agent = await getHttpAgent(source);
const { url } = getHttpsConnection(source);

const response = await axios.get(url, {
    httpsAgent: agent,
});

目前,除非源提供 HTTPS 客户端,否则无法访问非凭证类的源属性。例如,你将无法访问 PostgreSQL 源 上的 hostname 或其他非密钥属性。

5

用 OAuth 2.0 出向应用

用 OAuth 2.0 出向应用

要点:三种用法:预配置 client、原生 HTTP 手动注入 token、在动作里使用。

如果你的外部 API 需要 OAuth 2.0 授权,你可以在 Control Panel 中配置一个出站应用(outbound application),并将其用作 REST API 源的认证方法。当你的函数运行时,源会将调用用户的 OAuth 访问令牌作为会话凭证暴露出来。你的函数随后可以使用该令牌,代表用户调用外部 API。

这种模式在 Python 与 TypeScript v2 函数中受支持。代码示例如下,见使用源预配置的客户端

Limitations

  • TypeScript v1: TypeScript v1 函数无法直接从容中获取 OAuth 令牌。要从 TypeScript v1 函数认证一个 OAuth 2.0 API,请将调用包装在配置了出站应用的 REST API 源上的一个 webhook 中。如需直接获取令牌,请考虑迁移到 TypeScript v2
  • 部署模式: 当函数在部署模式下运行时,OAuth 令牌刷新不可用。如果调用用户的访问令牌在执行期间过期,函数无法自动刷新它。请在 serverless 模式 下运行函数,以使用 OAuth 支撑的出站应用。
  • 在 Workshop 中直接使用函数: 直接在 Workshop 模块中使用的函数,例如函数支撑的变量或填充组件内容的函数,无法触发 OAuth 2.0 的交互式授权提示。如果用户尚未授权该出站应用,函数会失败,而非显示提示。要从 Workshop 使用 OAuth 支撑的函数,请将其包装在一个函数支撑的动作中。或者,确保在函数于 Workshop 中被直接调用之前,用户已从另一个交互式界面(例如针对同一出站应用的函数支撑动作)完成授权流程。

Use the source's pre-configured client

最简单的方法是使用源提供的 HTTP 客户端。Authorization 头会被自动注入。

from functions.api import function
from functions.sources import get_source

@function(sources=["myOAuthSourceAlias"])
def call_external_api() -> str:
    source = get_source("myOAuthSourceAlias")
    url = source.get_https_connection().url
    client = source.get_https_connection().get_client()

    response = client.get(url + "/api/v1/resource", timeout=10)
    return response.text
import { getSource, getHttpsConnection, getFetch } from "@palantir/functions-sources";

export const config = {
    sources: ["myOAuthSourceAlias"]
};

export default async function callExternalApi(): Promise<string> {
    const source = await getSource("myOAuthSourceAlias");
    const { url } = getHttpsConnection(source);
    const fetch = await getFetch(source);

    const response = await fetch(url + "/api/v1/resource");

    return response.text();
}

Use a native HTTP client with manual token injection

如果你需要使用自己的 HTTP 客户端而非源提供的那个,请从会话凭证中获取 OAuth 令牌,并手动设置 Authorization 头。

import requests
from functions.api import function
from functions.sources import get_source
from external_systems.sources import OauthCredentials, Refreshable, SourceCredentials

@function(sources=["myOAuthSourceAlias"])
def call_external_api() -> str:
    source = get_source("myOAuthSourceAlias")
    url = source.get_https_connection().url

    refreshable_credentials: Refreshable[SourceCredentials] = source.get_session_credentials()
    session_credentials: SourceCredentials = refreshable_credentials.get()

    if not isinstance(session_credentials, OauthCredentials):
        raise ValueError("Expected OAuth credentials")

    access_token: str = session_credentials.access_token

    response = requests.get(
        url + "/api/v1/resource",
        headers={"Authorization": f"Bearer {access_token}"},
        timeout=10,
    )
    return response.text
import { getSource, getHttpsConnection } from "@palantir/functions-sources";

export const config = {
    sources: ["myOAuthSourceAlias"]
};

export default async function callExternalApi(): Promise<string> {
    const source = await getSource("myOAuthSourceAlias");
    const credentials = await source.sessionCredentials?.get();

    if (!credentials || credentials.type !== "oauth") {
        throw new Error("Expected OAuth credentials");
    }

    const accessToken: string = credentials.accessToken;
    const { url } = getHttpsConnection(source);

    const response = await fetch(url + "/api/v1/resource", {
        headers: { Authorization: `Bearer ${accessToken}` },
    });

    return response.text();
}

Use OAuth-backed functions in actions

一个常见模式是调用一个 OAuth 支撑的外部 API,并将结果喂给一个 Ontology 编辑。随后你可以通过一个函数支撑的动作暴露该函数。当用户从 Workshop 或 AIP Studio 运行该动作时,他们的 OAuth 令牌被用于发起 API 调用,所产生的对象编辑也归属于他们。

例如,下面的函数使用 OAuth 令牌从第三方身份服务获取调用用户的资料,然后使用该信息创建一个新的 Ontology 对象:

from functions.api import function, OntologyEdit
from functions.sources import get_source
from ontology_sdk import FoundryClient
from ontology_sdk.ontology.objects import UserProfile


@function(sources=["myOAuthSourceAlias"], edits=[UserProfile])
def link_user_profile() -> list[OntologyEdit]:
    source = get_source("myOAuthSourceAlias")
    url = source.get_https_connection().url
    client = source.get_https_connection().get_client()

    response = client.get(url + "/v1/me", timeout=10)
    response.raise_for_status()
    profile = response.json()

    ontology_edits = FoundryClient().ontology.edits()
    ontology_edits.objects.UserProfile.create(
        profile["id"],
        display_name=profile["display_name"],
    )
    return ontology_edits.get_edits()
import { getSource, getHttpsConnection, getFetch } from "@palantir/functions-sources";
import { UserProfile } from "@ontology/sdk";
import { Client } from "@osdk/client";
import { createEditBatch, Edits } from "@osdk/functions";

type OntologyEdit = Edits.Object<UserProfile>;

export const config = {
    sources: ["myOAuthSourceAlias"],
    edits: [UserProfile],
};

export default async function linkUserProfile(client: Client): Promise<OntologyEdit[]> {
    const source = await getSource("myOAuthSourceAlias");
    const { url } = getHttpsConnection(source);
    const fetch = await getFetch(source);

    const response = await fetch(url + "/v1/me");
    if (!response.ok) {
        throw new Error(`Failed to fetch profile: ${response.status}`);
    }
    const profile = await response.json();

    const batch = createEditBatch<OntologyEdit>(client);
    batch.create(UserProfile, {
        userProfileId: profile.id,
        displayName: profile.display_name,
    });
    return batch.getEdits();
}
6

常见错误排查

常见错误排查

要点:HTTP 407 代理认证等高频问题的处理。

对于 OAuth 授权错误,例如 HTTP 401: UnauthorizedCredentials expired and no refresh handler provided,或 Resolved source credentials are not present on the Source,请参阅 Data Connection 故障排查参考中的 OAuth 与出站应用

HTTP 407: Proxy authentication required

函数的网络请求必须被你的源的出口策略(egress policies)覆盖。如果目标主机名与允许的策略不匹配,请求可能返回 HTTP 407: Proxy Authentication Required

如果你的出口策略看起来正确,请检查请求 URL 是如何构建的。getHttpsConnection() 返回的 URL 没有尾部斜杠,因此附加的路径如果省略了前导 /,会被拼接到主机名上:

"https://example.com" + "api/v1"
→ "https://example.comapi/v1"

结果的主机名(example.comapi)不被任何出口策略覆盖,因此请求被拒绝。请在路径前加上 /(例如 url + "/api/v1")。

延伸阅读 · 相关页面

按主题横向跳转,不必顺着目录一篇篇读。

常见问题速答 · FAQ

关于「从函数里调用外部 API」,读者最常问的几个问题。

配置外部 API 访问是什么?
第一步永远是配置,代码之前先把通路打通。默认情况下,函数不允许调用外部 API。要启用从函数调用外部系统,你必须在 Data Connection 中配置一个源,以允许 Foundry 与该外部系统连接。
在函数里使用外部源是什么?
配置好之后,代码里如何引用。要从函数发起 API 调用,你必须首先使用资源导入侧边栏将你的源导入一个函数仓库。对于 TypeScript v2 与 Python 函数,我们随后建议创建一个源别名,并使用其别名键作为源标识符。
访问源属性与凭据是什么?
怎么拿到地址和凭据(注意:不写死在代码里)。你可以访问每种函数类型对应库所提供的源属性。
使用预配置客户端是什么?
直接用平台准备好的 client,省去认证细节。对于提供 REST API 的源,源对象允许你获取一个客户端。该客户端会预配置源上指定的服务端证书与客户端证书。它还会包含额外的代理配置,允许从函数执行所在环境向外出口(egress)。