Destination Service fails to authenticate
維護者通常 1 天內回覆
還沒有人認領這個 Issue。
評估
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 新手友好度
- 48/100
- Issue 類型
- 缺陷
- 描述清晰度
- 基本清楚
- 活躍度
- 活躍
- 技術堆疊
- java
- 領域
- authentication, backend, cloud
研究方向
Start at lookupDestinationByName in src/main/java/com/sap/oc/sdk/handler/OutputControlClient.java around line 328, then inspect the DestinationOptions configured for subscriber-only retrieval and user-token forwarding. Reproduce the multitenant BTP setup described in the issue and verify that the subscriber destination can obtain a token and successfully call the OData API without a 401 invalid_client error.
由索引模型根據 Issue 內容生成。
描述
Describe the Bug
Destination service is unable to call the API provided in the BTP Destination.
We get the error when accessing the destination maintained in the BTP subscriber subaccount even though destination is added as subscription dependency under multitenancy section.
Http status code 401. Response body '{"error":"invalid_client","error_description":"Bad credentials"}'
caused by: com.sap.cloud.security.xsuaa.client.OAuth2ServiceException: Error requesting access token!. Server URI https://test-app-provider.authentication.eu12.hana.ondemand.com/oauth/token. Http status code 401. Response body '{"error":"invalid_client","error_description":"Bad credentials"}'. Request Headers [X-zid: 2b0a175e-0f9a-46e7-a62c-40f3c3bf8c0b, Content-Type: application/x-www-form-urlencoded, X-CorrelationID: 3266bc7e-dd0b-4df3-4a64-6099d75968ee, Accept: application/json, User-Agent: token-client/3.6.5]. Response Headers [cache-control: no-store, content-type: application/json, date: Tue, 29 Sep 2026 11:25:56 GMT, pragma: no-cache, server: openresty, www-authenticate: Basic realm="UAA/client", error="unauthorized", error_description="Bad credentials", x-content-type-options: nosniff, x-frame-options: DENY, x-vcap-request-id: 4df39f45-d6a5-4d17-534c-9faf8e7a7235, x-xss-protection: 0, transfer-encoding: chunked, strict-transport-security: max-age=31536000; includeSubDomains; preload;]
Steps to Reproduce
We can help to reproduce the issue as the setup is a bit complicated.
However, this is the code that throws this error: https://github.tools.sap/erp4sme/suite-output-control-java-sdk/blob/15fbdd30a92ab0da910568954e64b4e3ced9870c/src/main/java/com/sap/oc/sdk/handler/OutputControlClient.java#L328
Expected Behavior
The destination should be picked up from the subscriber subaccount and the OData APIs using the destination URI should be called successfully
Screenshots
No response
Used Versions
- Java and Maven version via
mvn --version:- Apache Maven 3.9.12 (848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1)
- Java version: 21.0.10
- SAP Cloud SDK version: 5.26.0
- Spring Boot or CAP version:
- Spring Boot: 3.5.5
- CAP Java SDK: 4.6.1
Dependency tree via mvn dependency:tree
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.sap.soc:suite-output-control-java-sdk >--------------
[INFO] Building Suite Output Control Java SDK 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- dependency:3.7.0:tree (default-cli) @ suite-output-control-java-sdk ---
[INFO] com.sap.soc:suite-output-control-java-sdk:jar:1.0.0
[INFO] +- com.sap.cloud.sdk.cloudplatform:connectivity-destination-service:jar:5.26.0:compile
[INFO] | +- com.sap.cloud.sdk.cloudplatform:cloudplatform-core:jar:5.26.0:compile
[INFO] | +- com.sap.cloud.sdk.cloudplatform:caching:jar:5.26.0:compile
[INFO] | +- com.sap.cloud.sdk.cloudplatform:cloudplatform-connectivity:jar:5.26.0:compile
[INFO] | | +- com.sap.cloud.sdk.cloudplatform:resilience-api:jar:5.26.0:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.21.0:compile
[INFO] | | | \- com.fasterxml.jackson.core:jackson-annotations:jar:2.21:compile
[INFO] | | +- com.mikesamuel:json-sanitizer:jar:1.2.3:compile
[INFO] | | +- org.bouncycastle:bcprov-jdk18on:jar:1.83:compile
[INFO] | | \- org.bouncycastle:bcpkix-jdk18on:jar:1.83:compile
[INFO] | | \- org.bouncycastle:bcutil-jdk18on:jar:1.83:compile
[INFO] | +- com.sap.cloud.sdk.cloudplatform:connectivity-apache-httpclient4:jar:5.26.0:compile
[INFO] | +- com.sap.cloud.sdk.cloudplatform:connectivity-oauth:jar:5.26.0:compile
[INFO] | | +- com.sap.cloud.security.xsuaa:token-client:jar:3.6.5:compile
[INFO] | | | \- org.json:json:jar:20250517:compile
[INFO] | | +- com.sap.cloud.security:java-api:jar:3.6.5:compile
[INFO] | | \- com.sap.cloud.security:java-security:jar:3.6.5:runtime
[INFO] | | \- com.sap.cloud.security:env:jar:3.6.5:runtime
[INFO] | | +- com.sap.cloud.environment.servicebinding:java-sap-vcap-services:jar:0.21.0:runtime
[INFO] | | \- com.sap.cloud.environment.servicebinding:java-sap-service-operator:jar:0.21.0:runtime
[INFO] | +- com.sap.cloud.sdk.cloudplatform:resilience:jar:5.26.0:compile
[INFO] | | \- com.sap.cloud.sdk.cloudplatform:resilience4j:jar:5.26.0:compile
[INFO] | | +- io.github.resilience4j:resilience4j-core:jar:2.3.0:compile
[INFO] | | | \- org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar:1.9.0:compile
[INFO] | | | +- org.jetbrains.kotlin:kotlin-stdlib:jar:1.9.0:compile
[INFO] | | | | +- org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.9.0:compile
[INFO] | | | | \- org.jetbrains:annotations:jar:13.0:compile
[INFO] | | | \- org.jetbrains.kotlin:kotlin-stdlib-jdk7:jar:1.9.0:compile
[INFO] | | +- io.github.resilience4j:resilience4j-circuitbreaker:jar:2.3.0:compile
[INFO] | | +- io.github.resilience4j:resilience4j-bulkhead:jar:2.3.0:compile
[INFO] | | +- io.github.resilience4j:resilience4j-timelimiter:jar:2.3.0:compile
[INFO] | | +- io.github.resilience4j:resilience4j-retry:jar:2.3.0:compile
[INFO] | | +- io.github.resilience4j:resilience4j-ratelimiter:jar:2.3.0:compile
[INFO] | | \- javax.cache:cache-api:jar:1.1.1:compile
[INFO] | +- com.sap.cloud.sdk.cloudplatform:security:jar:5.26.0:compile
[INFO] | +- com.sap.cloud.sdk.cloudplatform:tenant:jar:5.26.0:compile
[INFO] | +- org.slf4j:slf4j-api:jar:2.0.17:compile
[INFO] | +- org.slf4j:jcl-over-slf4j:jar:2.0.17:runtime
[INFO] | +- com.google.guava:guava:jar:33.5.0-jre:compile
[INFO] | | +- com.google.guava:failureaccess:jar:1.0.3:compile
[INFO] | | +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:compile
[INFO] | | +- org.jspecify:jspecify:jar:1.0.0:compile
[INFO] | | +- com.google.errorprone:error_prone_annotations:jar:2.41.0:compile
[INFO] | | \- com.google.j2objc:j2objc-annotations:jar:3.1:compile
[INFO] | +- com.github.ben-manes.caffeine:caffeine:jar:3.2.3:compile
[INFO] | +- com.auth0:java-jwt:jar:4.5.0:compile
[INFO] | | \- com.fasterxml.jackson.core:jackson-core:jar:2.15.4:compile
[INFO] | +- commons-io:commons-io:jar:2.21.0:compile
[INFO] | +- org.apache.httpcomponents:httpcore:jar:4.4.16:compile
[INFO] | +- org.apache.httpcomponents:httpclient:jar:4.5.14:compile
[INFO] | | \- commons-codec:commons-codec:jar:1.11:compile
[INFO] | +- io.vavr:vavr:jar:0.11.0:compile
[INFO] | | \- io.vavr:vavr-match:jar:0.11.0:compile
[INFO] | +- org.apache.commons:commons-lang3:jar:3.20.0:compile
[INFO] | +- com.sap.cloud.environment.servicebinding.api:java-core-api:jar:0.21.0:compile
[INFO] | +- com.sap.cloud.environment.servicebinding.api:java-access-api:jar:0.21.0:compile
[INFO] | \- com.sap.cloud.environment.servicebinding.api:java-consumption-api:jar:0.21.0:compile
[INFO] +- com.google.code.gson:gson:jar:2.10.1:compile
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:5.10.2:test
[INFO] | +- org.opentest4j:opentest4j:jar:1.3.0:test
[INFO] | +- org.junit.platform:junit-platform-commons:jar:1.10.2:test
[INFO] | \- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] +- org.junit.jupiter:junit-jupiter-engine:jar:5.10.2:test
[INFO] | \- org.junit.platform:junit-platform-engine:jar:1.10.2:test
[INFO] +- org.mockito:mockito-core:jar:5.14.2:test
[INFO] | \- org.objenesis:objenesis:jar:3.3:test
[INFO] +- org.mockito:mockito-junit-jupiter:jar:5.14.2:test
[INFO] +- net.bytebuddy:byte-buddy:jar:1.17.5:test
[INFO] +- net.bytebuddy:byte-buddy-agent:jar:1.17.5:test
[INFO] \- org.assertj:assertj-core:jar:3.25.3:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.627 s
[INFO] Finished at: 2026-09-30T15:54:09+05:30
[INFO] ------------------------------------------------------------------------
Code Examples
Method name: lookupDestinationByName
package com.sap.oc.sdk.handler;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultCsrfTokenRetriever;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationOptions;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationService;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceOptionsAugmenter;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceRetrievalStrategy;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationServiceTokenExchangeStrategy;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
import com.sap.oc.sdk.exception.OutputControlException;
/**
* Base class for all OData handlers in the Output Control SDK.
*
* <p>
* Provides common HTTP primitives (GET, POST, PATCH, DELETE) for invoking
* the Output Control OData v4 service, along with shared infrastructure such
* as:
* <ul>
* <li>Standardized request construction and header management.</li>
* <li>CSRF token handling with automatic refresh on 403 responses.</li>
* <li>ETag / {@code If-Match} handling for concurrency-safe updates.</li>
* <li>JSON (de)serialization via a shared {@link Gson} instance.</li>
* <li>Consistent error handling and mapping to
* {@link OutputControlException}.</li>
* </ul>
* Concrete handler classes extend this base to implement entity-specific
* operations against the Output Control service.
*/
public abstract class OutputControlClient {
// logger used for diagnostic and error logging across all subclasses.
private static final Logger LOGGER = LoggerFactory.getLogger(OutputControlClient.class);
// BTP destination name used to connect to the Output Management Service on ABAP
// Cloud via the SDK.
private static final String DESTINATION_NAME = "output-management-service";
// System property to be set for local development
private static final String ENV_FALLBACK_ENABLED = "DESTINATION_FALLBACK_ENABLED";
// Destination lookup options: subscriber-only tenant isolation + user-token forwarding.
private static final DestinationOptions DESTINATION_OPTIONS = DestinationOptions.builder()
.augmentBuilder(DestinationServiceOptionsAugmenter.augmenter()
.retrievalStrategy(DestinationServiceRetrievalStrategy.ONLY_SUBSCRIBER)
.tokenExchangeStrategy(DestinationServiceTokenExchangeStrategy.FORWARD_USER_TOKEN))
.build();
// Base OData v4 service path for the Output Control API.
private static final String SERVICE_PATH = "/sap/opu/odata4/sap/api_outputcontroloutputrequest/srvd_a2x/sap/api_outputcontroloutputrequest/0001";
// Standard HTTP header names used throughout the OData requests.
private static final String HEADER_ACCEPT = "Accept";
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String HEADER_X_CSRF_TOKEN = "X-CSRF-Token";
private static final String HEADER_IF_MATCH = "If-Match";
// Shared Gson instance used for (de)serializing JSON request/response bodies.
protected final Gson gson;
// CSRF token cached per client instance; invalidated (reset to null) upon 403
// responses.
private final AtomicReference<String> cachedCsrfToken = new AtomicReference<>(null);
protected OutputControlClient() {
// Initialize the shared Gson instance with default configuration.
this.gson = new GsonBuilder().create();
}
// ==================== Destination resolution ====================
/**
*
* <p>
* This destination must be configured in the BTP subaccount with
* a technical user and is shared across all consuming CAP applications.
*/
protected String resolveDestinationName() {
return DESTINATION_NAME;
}
/**
* Resolves a destination by name in the currently active tenant context.
* <p>
* Prefers the BTP {@link DestinationService} (tenant-aware)
* <p>
* For local development only: set {@code DESTINATION_FALLBACK_ENABLED=true}
* to fall back to {@link DestinationAccessor} when the DestinationService
* is unavailable.
*
* @param destinationName the name of the destination to look up.
* @return the resolved HTTP {@link Destination}.
* @throws IllegalStateException if destination resolution fails and fallback is
* not enabled.
*/
private static Destination lookupDestinationByName(String destinationName) {
try {
// tenant-aware lookup via the BTP Destination Service.
return new DestinationService().tryGetDestination(destinationName, DESTINATION_OPTIONS).get().asHttp();
} catch (Exception e) {
// Fallback to be enabled for local development
boolean fallbackEnabled = Boolean.parseBoolean(System.getProperty(ENV_FALLBACK_ENABLED, "false"));
if (!fallbackEnabled) {
throw new IllegalStateException(
String.format("Failed to resolve destination '%s' via DestinationService. ", destinationName),
e);
}
LOGGER.warn("Falling back to DestinationAccessor for destination '{}': {}",
destinationName, e.getMessage());
return DestinationAccessor.getDestination(destinationName).asHttp();
}
}
// ==================== HTTP primitives ====================
/**
* Executes an HTTP GET request against the given relative service path.
*
* @param path relative path appended to the base OData service path.
* @return the response body as a JSON string.
*/
protected String httpGet(String path) {
return executeRequest("GET", path, null, false, null);
}
/**
* Executes an HTTP POST request with the given JSON body.
*
* @param path relative path appended to the base OData service path.
* @param body request payload object to be serialized as JSON.
* @return the response body as a JSON string.
*/
protected String httpPost(String path, Object body) {
return executeRequest("POST", path, body == null ? java.util.Collections.emptyMap() : body, true, null);
}
/**
* Executes an HTTP PATCH request with the given JSON body and optional ETag.
* <p>
* When {@code odataEtag} is non-null and non-blank it is sent as the
* {@code If-Match} header value. When it is {@code null} or blank the
* {@code If-Match} header is set to {@code *} (wildcard), which bypasses
* optimistic-concurrency checks on the server side.
*
* @param path relative path appended to the base OData service path.
* @param body request payload object to be serialized as JSON.
* @param odataEtag OData ETag value used in the {@code If-Match} header;
* pass {@code null} to send the wildcard value {@code *}.
* @return the response body as a JSON string.
*/
protected String httpPatch(String path, Object body, String odataEtag) {
return executeRequest("PATCH", path, body == null ? java.util.Collections.emptyMap() : body, true, odataEtag);
}
/**
* Executes an HTTP DELETE request against the given relative service path.
*
* @param path relative path appended to the base OData service path.
*/
protected void httpDelete(String path) {
executeRequest("DELETE", path, null, true, null);
}
/**
* Central request execution method.
* <p>
* Performs per-request destination resolution, builds the
* HTTP client, delegates to {@link #doExecute}, and handles CSRF token
* refresh on 403 responses.
*
* @param httpMethod the HTTP method ("GET", "POST", "PATCH", "DELETE").
* @param path relative path appended to the base OData service path.
* @param body optional request body object; will be serialized to JSON.
* @param csrfRequired whether a CSRF token and {@code If-Match} header are
* required.
* @param odataEtag OData ETag value for {@code If-Match}
* @return the response body as a JSON string.
* @throws OutputControlException if the destination cannot be resolved or the
* request fails.
*/
private String executeRequest(String httpMethod, String path, Object body, boolean csrfRequired, String odataEtag) {
String fullPath = SERVICE_PATH + path;
// Resolve destination NAME per request from the environment variable.
String destinationName = resolveDestinationName();
// Resolve destination in the currently active tenant context.
Destination destination = lookupDestinationByName(destinationName);
if (destination == null) {
throw new OutputControlException(
"Destination '" + destinationName + "' could not be resolved for current request context", null);
}
// Obtain a HttpClient
HttpClient httpClient = HttpClientAccessor.getHttpClient(destination);
try {
return executeRequest(httpClient, httpMethod, fullPath, body, csrfRequired, odataEtag);
} catch (OutputControlException e) {
// Log structured error information, then rethrow as-is.
LOGGER.error("{} {} failed: status=", httpMethod, fullPath, e.getStatusCode(), e.getMessage());
throw e;
} catch (Exception e) {
// Wrap any unexpected exception into an OutputControlException.
LOGGER.error("{} {} failed", httpMethod, fullPath, e);
throw new OutputControlException(httpMethod + " request failed: " + e.getMessage(), e);
}
}
/**
* Executes the request and, for CSRF-guarded calls, if the request fails with
* HTTP 403,
* then attempts the request another time with fresh CSRF token
*
* @param httpClient destination-aware HTTP client.
* @param httpMethod HTTP method.
* @param fullPath fully-qualified request URI.
* @param body optional request payload.
* @param csrfRequired whether a CSRF token must be attached.
* @param odataEtag OData ETag value for {@code If-Match}; wildcard is used
* if blank.
* @return the response body as a JSON string.
* @throws IOException if the underlying HTTP call fails.
*/
private String executeRequest(HttpClient httpClient, String httpMethod, String fullPath,
Object body, boolean csrfRequired, String odataEtag) throws IOException {
try {
return doExecute(httpClient, httpMethod, fullPath, body, csrfRequired, odataEtag);
} catch (OutputControlException e) {
// On 403 for CSRF-guarded requests, invalidate the cached token and retry once.
if (csrfRequired && e.getStatusCode() == 403) {
cachedCsrfToken.set(null);
return doExecute(httpClient, httpMethod, fullPath, body, csrfRequired, odataEtag);
}
throw e;
}
}
/**
* Builds and dispatches a single HTTP request via the given {@link HttpClient}.
*
* <p>
* Header attachment rules:
* <ul>
* <li><b>CSRF token</b> – attached whenever {@code csrfRequired} is
* {@code true}
* (i.e. POST, PATCH, DELETE).</li>
* <li><b>{@code If-Match}</b> – attached only for {@code PATCH} and
* {@code DELETE},
* where optimistic-concurrency control is meaningful.</li>
* </ul>
*
* @param httpClient pre-configured, destination-aware HTTP client.
* @param httpMethod the HTTP method to execute.
* @param fullPath the fully-qualified service path (including base path).
* @param body optional request body; ignored for GET/DELETE.
* @param csrfRequired whether a CSRF token must be attached.
* @param odataEtag OData ETag value for {@code If-Match} (PATCH/DELETE
* only);
* {@code null} sends the wildcard value {@code *}.
* @return the response body as a JSON string.
* @throws IOException if the underlying HTTP call fails.
*/
private String doExecute(HttpClient httpClient, String httpMethod, String fullPath,
Object body, boolean csrfRequired, String odataEtag) throws IOException {
RequestBuilder requestBuilder = createRequestBuilder(httpMethod, fullPath)
.addHeader(HEADER_ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
// ── CSRF token ────────────────────────────────────────────────────────────
// Required for all state-mutating requests (POST, PATCH, DELETE).
if (csrfRequired) {
String token = cachedCsrfToken.get();
if (token == null) {
String freshToken = new DefaultCsrfTokenRetriever()
.retrieveCsrfToken(httpClient, SERVICE_PATH)
.getToken();
// Only write if still null — if another thread already stored a token, tat thread will win
cachedCsrfToken.compareAndSet(null, freshToken);
token = cachedCsrfToken.get();
}
requestBuilder.addHeader(HEADER_X_CSRF_TOKEN, token);
}
// ── If-Match header ───────────────────────────────────────────────────────
// PATCH / DELETE: use the caller-supplied ETag, falling back to wildcard.
// POST: always sent as wildcard (*)
if ("PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) {
String ifMatch = (odataEtag != null && !odataEtag.isBlank()) ? odataEtag : "*";
requestBuilder.addHeader(HEADER_IF_MATCH, ifMatch);
} else if ("POST".equals(httpMethod)) {
requestBuilder.addHeader(HEADER_IF_MATCH, "*");
}
// ── Request body ──────────────────────────────────────────────────────────
if (body != null && ("POST".equals(httpMethod) || "PATCH".equals(httpMethod))) {
requestBuilder
.addHeader(HEADER_CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType())
.setEntity(new StringEntity(gson.toJson(body), ContentType.APPLICATION_JSON));
}
HttpResponse response = httpClient.execute(requestBuilder.build());
return ResponseParser.handleResponse(response);
}
/**
* Creates a new {@link RequestBuilder} for the given HTTP method and URI.
*
* @param httpMethod HTTP method name ({@code GET}, {@code POST}, {@code PATCH},
* {@code DELETE}).
* @param fullPath fully-qualified request URI.
* @return a configured {@link RequestBuilder}.
* @throws IllegalArgumentException if the HTTP method is not supported.
*/
private RequestBuilder createRequestBuilder(String httpMethod, String fullPath) {
// Select the appropriate RequestBuilder factory based on the HTTP verb.
RequestBuilder requestBuilder = switch (httpMethod) {
case "GET" -> RequestBuilder.get();
case "POST" -> RequestBuilder.post();
case "PATCH" -> RequestBuilder.patch();
case "DELETE" -> RequestBuilder.delete();
default -> throw new IllegalArgumentException("Unsupported HTTP method: " + httpMethod);
};
return requestBuilder.setUri(fullPath);
}
// ==================== Helper methods ====================
/**
* Returns the {@link Gson} instance used for JSON (de)serialization.
* <p>
* Exposed for use by subclass handlers that need to serialize custom
* payloads or parse response bodies.
*
* @return the {@link Gson} instance.
*/
protected Gson getGson() {
return gson;
}
}
Stack Trace
No response
Log File
test-obj-srv-2026-09-29 11_31_22.627+0000.txt
file
...
Affected Development Phase
Development
Impact
Blocked
Timeline
GA in 2702 release
- 主要語言
- Java
- 星號
- 41
- 分支
- 33
- 平均合併
- 22 小時 16 分鐘
- 30 天內合併 PR
- 19
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
SAP/cloud-sdk-java 的其他 Issue
-
question
難度 4/5 3-5 天 新手友好度 35/100
SAP/cloud-sdk-java#1301 ·
維護者通常 1 天內回覆
-
bug
難度 4/5 3-5 天 新手友好度 35/100
SAP/cloud-sdk-java#1300 ·
維護者通常 1 天內回覆
-
bug
難度 4/5 3-5 天 新手友好度 52/100
SAP/cloud-sdk-java#1297 · 2 則留言 ·
維護者通常 1 天內回覆
-
bug
難度 4/5 3-5 天 新手友好度 48/100
SAP/cloud-sdk-java#1290 · 3 則留言 ·
維護者通常 1 天內回覆
-
bug
難度 4/5 3-5 天 新手友好度 35/100
SAP/cloud-sdk-java#1289 · 1 則留言 ·
維護者通常 1 天內回覆
查看 SAP/cloud-sdk-java 的全部 Issue
相似的 Issue
-
Add Zammad可能已有人在做 @Arslan-TR 今天認領。 未關閉request
難度 2/5 1-3 小時 新手友好度 66/100
endoflife-date/endoflife.date#11298 · 1 則留言 ·
維護者通常 1 天內回覆
-
bug documentation
難度 2/5 1-3 小時 新手友好度 67/100
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 88/100
java-native-access/jna#1740 ·
-
Upgrade org.apache.felix.utils to 1.11.10可能已有人在做 @stataru8 今天認領。 未關閉
難度 2/5 1-3 小時 新手友好度 72/100
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 70/100
objectionary/eo#9329 ·
維護者通常 1 天內回覆