从函数里调用外部 API
函数可以调外部系统,但必须先在平台上配置数据源和凭据。这篇讲配置外部 API 访问、在函数里使用、OAuth 2.0,以及常见报错排查。
https://www.palantir.com/docs/foundry/functions/api-calls/
原始标题:Language-agnostic features > Make API calls from functions
先记住这几条
写在前面
可以从 TypeScript v1、TypeScript v2 与 Python 函数向外部源发起 API 调用,但这需要额外的配置。下面的内容与外部源的使用一起详细说明了这些配置。
:::callout{theme="neutral" title="Source aliases"}
对于 TypeScript v2 与 Python 函数,我们建议通过源别名(source aliases)引用源。源别名是一个可移植的、具名的引用,你可以将它作为源标识符,替代具体的源。当你的函数通过 Marketplace 产品 分发时,别名可以按环境重新映射到不同的源,从而保持你的函数代码可移植。TypeScript v1 函数使用生成的源符号,不支持别名。
配置外部 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 命令的文档来验证证书。
在函数里使用外部源
在函数里使用外部源
要从函数发起 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 调用,不能保证在所有环境中都有效。
访问源属性与凭据
访问源属性与凭据
你可以访问每种函数类型对应库所提供的源属性。
下面的示例展示了如何获取上面例子中源的 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")使用预配置客户端
使用预配置客户端
对于提供 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 或其他非密钥属性。
用 OAuth 2.0 出向应用
用 OAuth 2.0 出向应用
如果你的外部 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.textimport { 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.textimport { 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();
}常见错误排查
常见错误排查
对于 OAuth 授权错误,例如 HTTP 401: Unauthorized、Credentials 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」,读者最常问的几个问题。