diff --git a/src/main/java/org/cbioportal/legacy/web/util/InvolvedCancerStudyExtractorInterceptor.java b/src/main/java/org/cbioportal/legacy/web/util/InvolvedCancerStudyExtractorInterceptor.java index c98dd04e83d..7ef2c7db9df 100644 --- a/src/main/java/org/cbioportal/legacy/web/util/InvolvedCancerStudyExtractorInterceptor.java +++ b/src/main/java/org/cbioportal/legacy/web/util/InvolvedCancerStudyExtractorInterceptor.java @@ -35,6 +35,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -43,6 +44,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.cbioportal.application.rest.error.ErrorResponse; import org.cbioportal.legacy.model.AlterationFilter; import org.cbioportal.legacy.model.MolecularProfile; import org.cbioportal.legacy.model.MolecularProfileCaseIdentifier; @@ -158,6 +160,41 @@ public class InvolvedCancerStudyExtractorInterceptor implements HandlerIntercept "/treatments/patient-counts/fetch"; public static final String TREATMENTS_SAMPLE_COUNT_FETCH_PATH = "/treatments/sample-counts/fetch"; + /** + * Sends an HTTP 400 Bad Request response with a JSON error message. + * + * @param response the HttpServletResponse object + * @param message the error message to include in the response body + */ + private void sendBadRequestResponse(HttpServletResponse response, String message) { + if (response.isCommitted()) { + LOG.warn("Cannot send error response - response already committed"); + return; + } + + try { + // Null-safe handling for message (Exception.getMessage() can return null) + String safeMessage = message == null ? "Unknown error" : message; + + // Truncate message before Jackson source location info if present + int sourceIndex = safeMessage.indexOf(" at [Source:"); + if (sourceIndex != -1) { + safeMessage = safeMessage.substring(0, sourceIndex); + } + + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + // Use ObjectMapper for proper JSON escaping (matches GlobalExceptionHandler pattern) + ErrorResponse errorResponse = new ErrorResponse("Invalid request body: " + safeMessage); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + response.getWriter().flush(); + } catch (IOException ex) { + LOG.error("Failed to write error response: {}", ex.getMessage()); + } + } + @Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) { @@ -172,45 +209,45 @@ public boolean preHandle( requestPathInfo = requestPathInfo.replaceFirst("^/api", ""); // requestPathInfo = StringUtils.removeStart(requestPathInfo, "/column-store"); if (requestPathInfo.equals(PATIENT_FETCH_PATH)) { - return extractAttributesFromPatientFilter(request); + return extractAttributesFromPatientFilter(request, response); } else if (requestPathInfo.equals(SAMPLE_FETCH_PATH)) { - return extractAttributesFromSampleFilter(request); + return extractAttributesFromSampleFilter(request, response); } else if (requestPathInfo.equals(MOLECULAR_PROFILE_FETCH_PATH)) { - return extractAttributesFromMolecularProfileFilter(request); + return extractAttributesFromMolecularProfileFilter(request, response); } else if (requestPathInfo.equals(CLINICAL_ATTRIBUTE_COUNT_FETCH_PATH)) { - return extractAttributesFromClinicalAttributeCountFilter(request); + return extractAttributesFromClinicalAttributeCountFilter(request, response); } else if (requestPathInfo.equals(NAMESPACE_ATTRIBUTE_COUNT_FETCH_PATH)) { - return extractAttributesFromNamespaceAttributeCountFilter(request); + return extractAttributesFromNamespaceAttributeCountFilter(request, response); } else if (requestPathInfo.equals(CLINICAL_DATA_FETCH_PATH)) { - return extractAttributesFromClinicalDataMultiStudyFilter(request); + return extractAttributesFromClinicalDataMultiStudyFilter(request, response); } else if (requestPathInfo.equals(GENE_PANEL_DATA_FETCH_PATH)) { - return extractAttributesFromGenePanelDataMultipleStudyFilter(request); + return extractAttributesFromGenePanelDataMultipleStudyFilter(request, response); } else if (requestPathInfo.equals(MOLECULAR_DATA_MULTIPLE_STUDY_FETCH_PATH)) { - return extractAttributesFromMolecularDataMultipleStudyFilter(request); + return extractAttributesFromMolecularDataMultipleStudyFilter(request, response); } else if (requestPathInfo.equals(MUTATION_MULTIPLE_STUDY_FETCH_PATH)) { - return extractAttributesFromMutationMultipleStudyFilter(request); + return extractAttributesFromMutationMultipleStudyFilter(request, response); } else if (requestPathInfo.equals(COPY_NUMBER_SEG_FETCH_PATH)) { - return extractAttributesFromSampleIdentifiers(request); + return extractAttributesFromSampleIdentifiers(request, response); } else if (Arrays.asList( STUDY_VIEW_CLINICAL_DATA_BIN_COUNTS_PATH, STUDY_VIEW_CUSTOM_DATA_BIN_COUNTS_PATH) .contains(requestPathInfo)) { - return extractAttributesFromClinicalDataBinCountFilter(request); + return extractAttributesFromClinicalDataBinCountFilter(request, response); } else if (requestPathInfo.equals(STUDY_VIEW_GENOMICL_DATA_BIN_COUNTS_PATH)) { - return extractAttributesFromGenomicDataBinCountFilter(request); + return extractAttributesFromGenomicDataBinCountFilter(request, response); } else if (Arrays.asList( STUDY_VIEW_GENOMICL_DATA_COUNTS_PATH, STUDY_VIEW_MUTATION_DATA_COUNTS_PATH) .contains(requestPathInfo)) { - return extractAttributesFromGenomicDataCountFilter(request); + return extractAttributesFromGenomicDataCountFilter(request, response); } else if (requestPathInfo.equals(STUDY_VIEW_GENERIC_ASSAY_DATA_BIN_COUNTS_PATH)) { - return extractAttributesFromGenericAssayDataBinCountFilter(request); + return extractAttributesFromGenericAssayDataBinCountFilter(request, response); } else if (requestPathInfo.equals(STUDY_VIEW_GENERIC_ASSAY_DATA_COUNTS_PATH)) { - return extractAttributesFromGenericAssayDataCountFilter(request); + return extractAttributesFromGenericAssayDataCountFilter(request, response); } else if (Arrays.asList( STUDY_VIEW_CLINICAL_DATA_COUNTS_PATH, STUDY_VIEW_CUSTOM_DATA_COUNTS_PATH) .contains(requestPathInfo)) { - return extractAttributesFromClinicalDataCountFilter(request); + return extractAttributesFromClinicalDataCountFilter(request, response); } else if (requestPathInfo.equals(STUDY_VIEW_NAMESPACE_DATA_COUNTS_PATH)) { - return extractAttributesFromNamespaceDataCountFilter(request); + return extractAttributesFromNamespaceDataCountFilter(request, response); } else if (Arrays.asList( STUDY_VIEW_CLINICAL_DATA_DENSITY_PATH, STUDY_VIEW_CLINICAL_DATA_VIOLIN_PATH, @@ -229,31 +266,32 @@ public boolean preHandle( TREATMENTS_PATIENT_COUNT_FETCH_PATH, TREATMENTS_SAMPLE_COUNT_FETCH_PATH) .contains(requestPathInfo)) { - return extractAttributesFromStudyViewFilter(request); + return extractAttributesFromStudyViewFilter(request, response); } else if (requestPathInfo.equals(CLINICAL_DATA_ENRICHMENT_FETCH_PATH)) { - return extractAttributesFromGroupFilter(request); + return extractAttributesFromGroupFilter(request, response); } else if (requestPathInfo.equals(MUTATION_ENRICHMENT_FETCH_PATH) || requestPathInfo.equals(COPY_NUMBER_ENRICHMENT_FETCH_PATH) || requestPathInfo.equals(EXPRESSION_ENRICHMENT_FETCH_PATH) || requestPathInfo.equals(GENERIC_ASSAY_ENRICHMENT_FETCH_PATH) || requestPathInfo.equals(GENERIC_ASSAY_CATEGORICAL_ENRICHMENT_FETCH_PATH) || requestPathInfo.equals(GENERIC_ASSAY_BINARY_ENRICHMENT_FETCH_PATH)) { - return extractAttributesFromMolecularProfileCasesGroups(request); + return extractAttributesFromMolecularProfileCasesGroups(request, response); } else if (requestPathInfo.equals(ALTERATION_ENRICHMENT_FETCH_PATH)) { - return extractAttributesFromMolecularProfileCasesGroupsAndAlterationTypes(request); + return extractAttributesFromMolecularProfileCasesGroupsAndAlterationTypes(request, response); } else if (requestPathInfo.equals(STRUCTURAL_VARIANT_FETCH_PATH)) { - return extractAttributesFromStructuralVariantFilter(request); + return extractAttributesFromStructuralVariantFilter(request, response); } else if (requestPathInfo.equals(GENERIC_ASSAY_DATA_MULTIPLE_STUDY_FETCH_PATH)) { - return extractAttributesFromGenericAssayDataMultipleStudyFilter(request); + return extractAttributesFromGenericAssayDataMultipleStudyFilter(request, response); } else if (requestPathInfo.equals(SURVIVAL_DATA_FETCH_PATH)) { - return extractCancerStudyIdsFromSurvivalRequest(request); + return extractCancerStudyIdsFromSurvivalRequest(request, response); } else if (requestPathInfo.equals(CLINICAL_EVENT_META_FETCH_PATH)) { - return extractCancerStudyIdsFromClinicalEventAttributeRequest(request); + return extractCancerStudyIdsFromClinicalEventAttributeRequest(request, response); } return true; } - private boolean extractAttributesFromPatientFilter(HttpServletRequest request) { + private boolean extractAttributesFromPatientFilter( + HttpServletRequest request, HttpServletResponse response) { try { PatientFilter patientFilter = objectMapper.readValue(request.getInputStream(), PatientFilter.class); @@ -268,6 +306,7 @@ private boolean extractAttributesFromPatientFilter(HttpServletRequest request) { } } catch (Exception e) { LOG.error("exception thrown during extraction of patientFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -287,7 +326,8 @@ private Collection extractCancerStudyIdsFromPatientFilter(PatientFilter return studyIdSet; } - private boolean extractAttributesFromSampleFilter(HttpServletRequest request) { + private boolean extractAttributesFromSampleFilter( + HttpServletRequest request, HttpServletResponse response) { try { SampleFilter sampleFilter = objectMapper.readValue(request.getInputStream(), SampleFilter.class); @@ -302,6 +342,7 @@ private boolean extractAttributesFromSampleFilter(HttpServletRequest request) { } } catch (Exception e) { LOG.error("exception thrown during extraction of sampleFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -321,7 +362,8 @@ private Collection extractCancerStudyIdsFromSampleFilter(SampleFilter sa return studyIdSet; } - private boolean extractAttributesFromMolecularProfileFilter(HttpServletRequest request) { + private boolean extractAttributesFromMolecularProfileFilter( + HttpServletRequest request, HttpServletResponse response) { try { MolecularProfileFilter molecularProfileFilter = objectMapper.readValue(request.getInputStream(), MolecularProfileFilter.class); @@ -336,6 +378,7 @@ private boolean extractAttributesFromMolecularProfileFilter(HttpServletRequest r } } catch (Exception e) { LOG.error("exception thrown during extraction of molecularProfileFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -355,7 +398,8 @@ private Collection extractCancerStudyIdsFromMolecularProfileFilter( return studyIdSet; } - private boolean extractAttributesFromClinicalAttributeCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromClinicalAttributeCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { ClinicalAttributeCountFilter clinicalAttributeCountFilter = objectMapper.readValue(request.getInputStream(), ClinicalAttributeCountFilter.class); @@ -372,6 +416,7 @@ private boolean extractAttributesFromClinicalAttributeCountFilter(HttpServletReq } catch (Exception e) { LOG.error( "exception thrown during extraction of clinicalAttributeCountFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -392,7 +437,8 @@ private Collection extractCancerStudyIdsFromClinicalAttributeCountFilter return studyIdSet; } - private boolean extractAttributesFromNamespaceAttributeCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromNamespaceAttributeCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { NamespaceAttributeCountFilter namespaceAttributeCountFilter = objectMapper.readValue(request.getInputStream(), NamespaceAttributeCountFilter.class); @@ -409,7 +455,9 @@ private boolean extractAttributesFromNamespaceAttributeCountFilter(HttpServletRe } } catch (Exception e) { LOG.error( - "exception thrown during extraction of clinicalAttributeCountFilter: {}", e.getMessage()); + "exception thrown during extraction of namespaceAttributeCountFilter: {}", + e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -425,7 +473,8 @@ private Collection extractCancerStudyIdsFromNamespaceAttributeCountFilte return studyIdSet; } - private boolean extractAttributesFromClinicalDataMultiStudyFilter(HttpServletRequest request) { + private boolean extractAttributesFromClinicalDataMultiStudyFilter( + HttpServletRequest request, HttpServletResponse response) { try { ClinicalDataMultiStudyFilter clinicalDataMultiStudyFilter = objectMapper.readValue(request.getInputStream(), ClinicalDataMultiStudyFilter.class); @@ -442,6 +491,7 @@ private boolean extractAttributesFromClinicalDataMultiStudyFilter(HttpServletReq } catch (Exception e) { LOG.error( "exception thrown during extraction of clinicalDataMultiStudyFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -460,7 +510,7 @@ private Collection extractCancerStudyIdsFromClinicalDataMultiStudyFilter } private boolean extractAttributesFromGenePanelDataMultipleStudyFilter( - HttpServletRequest request) { + HttpServletRequest request, HttpServletResponse response) { try { GenePanelDataMultipleStudyFilter genePanelDataMultipleStudyFilter = objectMapper.readValue(request.getInputStream(), GenePanelDataMultipleStudyFilter.class); @@ -481,6 +531,7 @@ private boolean extractAttributesFromGenePanelDataMultipleStudyFilter( LOG.error( "exception thrown during extraction of genePanelSampleMolecularIdentifiers: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -500,7 +551,7 @@ private Collection extractCancerStudyIdsFromGenePanelDataMultipleStudyFi } private boolean extractAttributesFromMolecularDataMultipleStudyFilter( - HttpServletRequest request) { + HttpServletRequest request, HttpServletResponse response) { try { MolecularDataMultipleStudyFilter molecularDataMultipleStudyFilter = objectMapper.readValue(request.getInputStream(), MolecularDataMultipleStudyFilter.class); @@ -521,6 +572,7 @@ private boolean extractAttributesFromMolecularDataMultipleStudyFilter( LOG.error( "exception thrown during extraction of molecularDataMultipleStudyFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -540,7 +592,7 @@ private Collection extractCancerStudyIdsFromMolecularDataMultipleStudyFi } private boolean extractAttributesFromGenericAssayDataMultipleStudyFilter( - HttpServletRequest request) { + HttpServletRequest request, HttpServletResponse response) { try { GenericAssayDataMultipleStudyFilter genericAssayDataMultipleStudyFilter = objectMapper.readValue( @@ -563,6 +615,7 @@ private boolean extractAttributesFromGenericAssayDataMultipleStudyFilter( LOG.error( "exception thrown during extraction of genericAssayDataMultipleStudyFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -581,7 +634,8 @@ private Collection extractCancerStudyIdsFromGenericAssayDataMultipleStud return studyIdSet; } - private boolean extractAttributesFromMutationMultipleStudyFilter(HttpServletRequest request) { + private boolean extractAttributesFromMutationMultipleStudyFilter( + HttpServletRequest request, HttpServletResponse response) { try { MutationMultipleStudyFilter mutationMultipleStudyFilter = objectMapper.readValue(request.getInputStream(), MutationMultipleStudyFilter.class); @@ -598,6 +652,7 @@ private boolean extractAttributesFromMutationMultipleStudyFilter(HttpServletRequ } catch (Exception e) { LOG.error( "exception thrown during extraction of mutationMultipleStudyFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -616,7 +671,8 @@ private Set extractCancerStudyIdsFromMutationMultipleStudyFilter( return studyIdSet; } - private boolean extractAttributesFromSampleIdentifiers(HttpServletRequest request) { + private boolean extractAttributesFromSampleIdentifiers( + HttpServletRequest request, HttpServletResponse response) { try { List sampleIdentifiers = Arrays.asList(objectMapper.readValue(request.getInputStream(), SampleIdentifier[].class)); @@ -631,12 +687,14 @@ private boolean extractAttributesFromSampleIdentifiers(HttpServletRequest reques } } catch (Exception e) { LOG.error("exception thrown during extraction of sampleIdentifiers: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromClinicalDataBinCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromClinicalDataBinCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { ClinicalDataBinCountFilter clinicalDataBinCountFilter = objectMapper.readValue(request.getInputStream(), ClinicalDataBinCountFilter.class); @@ -652,12 +710,14 @@ private boolean extractAttributesFromClinicalDataBinCountFilter(HttpServletReque } catch (Exception e) { LOG.error( "exception thrown during extraction of clinicalDataBinCountFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromGenomicDataBinCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromGenomicDataBinCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { GenomicDataBinCountFilter genomicDataBinCountFilter = objectMapper.readValue(request.getInputStream(), GenomicDataBinCountFilter.class); @@ -673,12 +733,14 @@ private boolean extractAttributesFromGenomicDataBinCountFilter(HttpServletReques } catch (Exception e) { LOG.error( "exception thrown during extraction of genomicDataBinCountFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromGenomicDataCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromGenomicDataCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { GenomicDataCountFilter genomicDataCountFilter = objectMapper.readValue(request.getInputStream(), GenomicDataCountFilter.class); @@ -693,12 +755,14 @@ private boolean extractAttributesFromGenomicDataCountFilter(HttpServletRequest r } } catch (Exception e) { LOG.error("exception thrown during extraction of genomicDataCountFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromGenericAssayDataBinCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromGenericAssayDataBinCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { GenericAssayDataBinCountFilter genericAssayDataBinCountFilter = objectMapper.readValue(request.getInputStream(), GenericAssayDataBinCountFilter.class); @@ -718,12 +782,14 @@ private boolean extractAttributesFromGenericAssayDataBinCountFilter(HttpServletR LOG.error( "exception thrown during extraction of genericAssayDataBinCountFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromGenericAssayDataCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromGenericAssayDataCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { GenericAssayDataCountFilter genericAssayDataCountFilter = objectMapper.readValue(request.getInputStream(), GenericAssayDataCountFilter.class); @@ -740,16 +806,18 @@ private boolean extractAttributesFromGenericAssayDataCountFilter(HttpServletRequ } catch (Exception e) { LOG.error( "exception thrown during extraction of genericAssayDataCountFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromClinicalDataCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromClinicalDataCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { ClinicalDataCountFilter clinicalDataCountFilter = objectMapper.readValue(request.getInputStream(), ClinicalDataCountFilter.class); - LOG.debug("extracted clinicalDataBinCountFilter: {}", clinicalDataCountFilter); + LOG.debug("extracted clinicalDataCountFilter: {}", clinicalDataCountFilter); LOG.debug("setting interceptedClinicalDataCountFilter to {}", clinicalDataCountFilter); request.setAttribute("interceptedClinicalDataCountFilter", clinicalDataCountFilter); if (cacheMapUtil.hasCacheEnabled()) { @@ -760,13 +828,15 @@ private boolean extractAttributesFromClinicalDataCountFilter(HttpServletRequest } } catch (Exception e) { LOG.error( - "exception thrown during extraction of clinicalDataBinCountFilter: {}", e.getMessage()); + "exception thrown during extraction of clinicalDataCountFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromNamespaceDataCountFilter(HttpServletRequest request) { + private boolean extractAttributesFromNamespaceDataCountFilter( + HttpServletRequest request, HttpServletResponse response) { try { NamespaceDataCountFilter namespaceDataCountFilter = objectMapper.readValue(request.getInputStream(), NamespaceDataCountFilter.class); @@ -782,12 +852,14 @@ private boolean extractAttributesFromNamespaceDataCountFilter(HttpServletRequest } catch (Exception e) { LOG.error( "exception thrown during extraction of namespaceDataCountFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromGroupFilter(HttpServletRequest request) { + private boolean extractAttributesFromGroupFilter( + HttpServletRequest request, HttpServletResponse response) { try { GroupFilter groupFilter = objectMapper.readValue(request.getInputStream(), GroupFilter.class); LOG.debug("extracted groupFilter: {}", groupFilter); @@ -805,12 +877,14 @@ private boolean extractAttributesFromGroupFilter(HttpServletRequest request) { } } catch (Exception e) { LOG.error("exception thrown during extraction of groupFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromStudyViewFilter(HttpServletRequest request) { + private boolean extractAttributesFromStudyViewFilter( + HttpServletRequest request, HttpServletResponse response) { try { StudyViewFilter studyViewFilter = objectMapper.readValue(request.getInputStream(), StudyViewFilter.class); @@ -835,12 +909,14 @@ private boolean extractAttributesFromStudyViewFilter(HttpServletRequest request) } } catch (Exception e) { LOG.error("exception thrown during extraction of studyViewFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromMolecularProfileCasesGroups(HttpServletRequest request) { + private boolean extractAttributesFromMolecularProfileCasesGroups( + HttpServletRequest request, HttpServletResponse response) { try { List molecularProfileCasesGroupFilters = Arrays.asList( @@ -863,13 +939,14 @@ private boolean extractAttributesFromMolecularProfileCasesGroups(HttpServletRequ LOG.error( "exception thrown during extraction of molecularProfileCasesGroupFilters: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } private boolean extractAttributesFromMolecularProfileCasesGroupsAndAlterationTypes( - HttpServletRequest request) { + HttpServletRequest request, HttpServletResponse response) { try { MolecularProfileCasesGroupAndAlterationTypeFilter molecularProfileCasesAndAlterationTypesGroupFilters = @@ -902,12 +979,14 @@ private boolean extractAttributesFromMolecularProfileCasesGroupsAndAlterationTyp LOG.error( "exception thrown during extraction of molecularProfileCasesGroupFilters: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } - private boolean extractAttributesFromStructuralVariantFilter(HttpServletRequest request) { + private boolean extractAttributesFromStructuralVariantFilter( + HttpServletRequest request, HttpServletResponse response) { try { StructuralVariantFilter structuralVariantFilter = objectMapper.readValue(request.getInputStream(), StructuralVariantFilter.class); @@ -928,6 +1007,7 @@ private boolean extractAttributesFromStructuralVariantFilter(HttpServletRequest } catch (Exception e) { LOG.error( "exception thrown during extraction of structuralVariantFilter: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; @@ -1081,7 +1161,8 @@ private Set extractCancerStudyIdsFromMolecularProfileCasesGroups( return studyIdSet; } - private boolean extractCancerStudyIdsFromSurvivalRequest(HttpServletRequest request) { + private boolean extractCancerStudyIdsFromSurvivalRequest( + HttpServletRequest request, HttpServletResponse response) { try { SurvivalRequest survivalRequest = objectMapper.readValue(request.getInputStream(), SurvivalRequest.class); @@ -1098,13 +1179,14 @@ private boolean extractCancerStudyIdsFromSurvivalRequest(HttpServletRequest requ } } catch (Exception e) { LOG.error("exception thrown during extraction of survivalRequest: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; } private boolean extractCancerStudyIdsFromClinicalEventAttributeRequest( - HttpServletRequest request) { + HttpServletRequest request, HttpServletResponse response) { try { ClinicalEventAttributeRequest clinicalEventAttributeRequest = objectMapper.readValue(request.getInputStream(), ClinicalEventAttributeRequest.class); @@ -1125,6 +1207,7 @@ private boolean extractCancerStudyIdsFromClinicalEventAttributeRequest( LOG.error( "exception thrown during extraction of clinicalEventAttributeRequest: {}", e.getMessage()); + sendBadRequestResponse(response, e.getMessage()); return false; } return true; diff --git a/src/test/java/org/cbioportal/legacy/web/util/InvolvedCancerStudyExtractorInterceptorTest.java b/src/test/java/org/cbioportal/legacy/web/util/InvolvedCancerStudyExtractorInterceptorTest.java new file mode 100644 index 00000000000..0fe592864c2 --- /dev/null +++ b/src/test/java/org/cbioportal/legacy/web/util/InvolvedCancerStudyExtractorInterceptorTest.java @@ -0,0 +1,166 @@ +package org.cbioportal.legacy.web.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import org.cbioportal.legacy.persistence.cachemaputil.CacheMapUtil; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.DelegatingServletInputStream; + +@RunWith(MockitoJUnitRunner.class) +public class InvolvedCancerStudyExtractorInterceptorTest { + + @InjectMocks private InvolvedCancerStudyExtractorInterceptor interceptor; + + @Mock private CacheMapUtil cacheMapUtil; + + @Mock private HttpServletRequest request; + + @Mock private HttpServletResponse response; + + @Spy private ObjectMapper objectMapper = new ObjectMapper(); + + private StringWriter responseWriter; + + @Before + public void setUp() throws IOException { + responseWriter = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(responseWriter)); + when(response.isCommitted()).thenReturn(false); + when(cacheMapUtil.hasCacheEnabled()).thenReturn(false); + } + + private ServletInputStream createInputStream(String content) { + ByteArrayInputStream byteStream = + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + return new DelegatingServletInputStream(byteStream); + } + + @Test + public void testMalformedJsonReturnsBadRequest() throws Exception { + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/api/patients/fetch"); + when(request.getInputStream()).thenReturn(createInputStream("{'invalid json'}")); + + boolean result = interceptor.preHandle(request, response, null); + + assertFalse("preHandle should return false for malformed JSON", result); + verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST); + verify(response).setContentType("application/json"); + assertTrue( + "Response should contain error message", + responseWriter.toString().contains("Invalid request body")); + } + + @Test + public void testEmptyBodyReturnsBadRequest() throws Exception { + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/api/samples/fetch"); + when(request.getInputStream()).thenReturn(createInputStream("")); + + boolean result = interceptor.preHandle(request, response, null); + + assertFalse("preHandle should return false for empty body", result); + verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @Test + public void testValidJsonPassesThrough() throws Exception { + String validJson = + "{\"sampleIdentifiers\":[{\"sampleId\":\"sample1\",\"studyId\":\"study1\"}]}"; + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/api/samples/fetch"); + when(request.getInputStream()).thenReturn(createInputStream(validJson)); + + boolean result = interceptor.preHandle(request, response, null); + + assertTrue("preHandle should return true for valid JSON", result); + verify(response, never()).setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @Test + public void testNonPostRequestPassesThrough() throws Exception { + when(request.getMethod()).thenReturn("GET"); + + boolean result = interceptor.preHandle(request, response, null); + + assertTrue("preHandle should return true for GET requests", result); + verify(response, never()).setStatus(anyInt()); + } + + @Test + public void testMessageTruncationRemovesJacksonDetails() throws Exception { + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/api/patients/fetch"); + when(request.getInputStream()).thenReturn(createInputStream("{bad}")); + + interceptor.preHandle(request, response, null); + + String responseBody = responseWriter.toString(); + assertFalse( + "Response should not contain Jackson source details", responseBody.contains("at [Source:")); + } + + @Test + public void testJsonEscapingForSpecialCharacters() throws Exception { + // This tests that ObjectMapper properly escapes special characters + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/api/patients/fetch"); + // JSON with unmatched quote that would cause parsing error with special chars in message + when(request.getInputStream()).thenReturn(createInputStream("{\"field\": \"value with \\n}")); + + interceptor.preHandle(request, response, null); + + String responseBody = responseWriter.toString(); + // Verify it's valid JSON (ObjectMapper would produce valid JSON) + assertTrue("Response should be valid JSON", responseBody.startsWith("{\"message\":")); + assertTrue("Response should end properly", responseBody.endsWith("}")); + } + + @Test + public void testCommittedResponseDoesNotWrite() throws Exception { + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/api/patients/fetch"); + when(request.getInputStream()).thenReturn(createInputStream("invalid")); + when(response.isCommitted()).thenReturn(true); + + interceptor.preHandle(request, response, null); + + verify(response, never()).setStatus(anyInt()); + verify(response, never()).getWriter(); + } + + @Test + public void testNullExceptionMessageHandledGracefully() throws Exception { + // Test that null message from Exception.getMessage() doesn't cause NPE + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/api/patients/fetch"); + // Throwing an exception that returns null for getMessage() + when(request.getInputStream()).thenThrow(new NullPointerException()); + + boolean result = interceptor.preHandle(request, response, null); + + assertFalse("preHandle should return false for exception", result); + verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST); + String responseBody = responseWriter.toString(); + assertTrue( + "Response should contain fallback message for null exception message", + responseBody.contains("Unknown error") || responseBody.contains("Invalid request body")); + } +}