From 66a1d9e1a0967b58a7f03ca8484c5ce3647a27a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 10:55:51 +0200 Subject: [PATCH] Keep embed params out of subresource self links findRelInternal builds the self link from the raw query string, so /items//bundles?embed=primaryBitstream reports a self link that is not the resource that was requested: GET /api/core/items//bundles?embed=primaryBitstream&size=10 self .../bundles?embed=primaryBitstream&size=10 The collection and search endpoints already exclude embed params, through getEncodedParameterStringFromRequestParams - added by PR #3105 to fix #3062, but never wired into the subresource path. That is why #8577 is still open. Filter embed and embed.* out of the query string before building the link. Every other parameter is passed through verbatim rather than rebuilt, so a client's own percent-encoding survives untouched. Related to DSpace/DSpace#8577 --- .../app/rest/RestResourceController.java | 29 +++++++++++++++++-- .../app/rest/BundleRestRepositoryIT.java | 23 +++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java index b82b4830753..b202443b431 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -20,9 +20,11 @@ import java.lang.reflect.Method; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -813,8 +815,8 @@ private RepresentationModel findRelInternal(HttpServle } Link link = null; - String querystring = request.getQueryString(); - if (querystring != null && querystring.length() > 0) { + String querystring = removeEmbedParamsFromQueryString(request.getQueryString()); + if (StringUtils.isNotEmpty(querystring)) { link = linkTo(this.getClass(), apiCategory, model).slash(uuid) .slash(subpath + '?' + querystring).withSelfRel(); } else { @@ -1024,6 +1026,29 @@ public RepresentationModel executeSearchMethods * @param parameters * @return encoded uriString containing request parameters without embed parameter */ + /** + * Remove the "embed" and "embed.*" parameters from a raw query string. + * + * Embedding a subresource doesn't change which resource was requested, so it doesn't belong in + * that resource's self link. The collection and search endpoints already leave these out, via + * {@link #getEncodedParameterStringFromRequestParams}; this does the same for the subresource + * lists, which build their self link from the raw query string. The remaining parameters are + * passed through untouched, so the client's own encoding is preserved. + * + * @param querystring the raw query string, may be null + * @return the query string without embed parameters, empty if nothing is left + */ + private String removeEmbedParamsFromQueryString(String querystring) { + if (StringUtils.isEmpty(querystring)) { + return ""; + } + return Arrays.stream(StringUtils.split(querystring, '&')) + .filter(param -> !StringUtils.equals(param, "embed") + && !StringUtils.startsWith(param, "embed=") + && !StringUtils.startsWith(param, "embed.")) + .collect(Collectors.joining("&")); + } + private String getEncodedParameterStringFromRequestParams( @RequestParam MultiValueMap parameters) { UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java index 6b148e88e13..0b501df508f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BundleRestRepositoryIT.java @@ -222,6 +222,29 @@ public void getItemBundles() throws Exception { ; } + @Test + public void getItemBundlesSelfLinkHasNoEmbedParams() throws Exception { + + context.turnOffAuthorisationSystem(); + + bundle1 = BundleBuilder.createBundle(context, item).withName("testname").build(); + + context.restoreAuthSystemState(); + + // embedding a subresource doesn't change which resource was requested, so the embed params + // must not end up in its self link - the other params must survive untouched + getClient().perform(get("/api/core/items/" + item.getID() + "/bundles") + .param("embed", "primaryBitstream") + .param("embed.size", "bitstreams=5") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/items/" + item.getID() + "/bundles"))) + .andExpect(jsonPath("$._links.self.href", Matchers.not(Matchers.containsString("embed")))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("size=10"))) + ; + } + @Test public void createBundleWithoutMetadata() throws Exception { ObjectMapper mapper = new ObjectMapper();