Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

package org.openmetadata.service.socket;

import java.net.URL;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponseWrapper;

import static org.openmetadata.service.exception.OMErrorPageHandler.setSecurityHeader;

import io.dropwizard.servlets.assets.AssetServlet;
Expand All @@ -28,6 +34,7 @@
public class OpenMetadataAssetServlet extends AssetServlet {
private final OMWebConfiguration webConfiguration;
private final String basePath;
private final String resourcePath;

public OpenMetadataAssetServlet(
String basePath,
Expand All @@ -36,6 +43,7 @@
@Nullable String indexFile,
OMWebConfiguration webConf) {
super(resourcePath, uriPath, indexFile, "text/html", StandardCharsets.UTF_8);
this.resourcePath = resourcePath;
this.webConfiguration = webConf;
this.basePath = basePath;
}
Expand All @@ -54,6 +62,37 @@
return;
}

String acceptEncoding = req.getHeader("Accept-Encoding");

// 1. Check for Brotli (br)
if (acceptEncoding != null && acceptEncoding.contains("br")) {
try {
String fullResourcePath = getPathToCheck(req, requestUri, ".br");
URL url = this.getClass().getResource(fullResourcePath);
if (url != null) {
serveCompressed(req, resp, requestUri, "br", "br");
return;
}
} catch (Exception e) {
// Ignore and try next
}
}

// 2. Check for Gzip
if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
try {
String fullResourcePath = getPathToCheck(req, requestUri, ".gz");
URL url = this.getClass().getResource(fullResourcePath);

if (url != null) {
serveCompressed(req, resp, requestUri, "gzip", "gz");
return;
}
} catch (Exception e) {
// Fallback to default behavior
}
}

super.doGet(req, resp);

// For SPA routing: serve index.html for 404s that don't look like static asset requests
Expand All @@ -69,6 +108,46 @@
}
}

private String getPathToCheck(HttpServletRequest req, String requestUri, String extension) {
String pathToCheck = requestUri;
String contextPath = req.getContextPath();
if (contextPath != null && requestUri.startsWith(contextPath)) {
pathToCheck = requestUri.substring(contextPath.length());
}
return this.resourcePath + (pathToCheck.startsWith("/") ? "" : "/") + pathToCheck + extension;
}

private void serveCompressed(HttpServletRequest req, HttpServletResponse resp, String requestUri, String contentEncoding, String extension) throws ServletException, IOException {
resp.setHeader("Content-Encoding", contentEncoding);
String mimeType = req.getServletContext().getMimeType(requestUri);

HttpServletRequestWrapper compressedReq = new HttpServletRequestWrapper(req) {
@Override
public String getPathInfo() {
String pathInfo = super.getPathInfo();
return pathInfo != null ? pathInfo + "." + extension : null;
}

@Override
public String getRequestURI() {
return super.getRequestURI() + "." + extension;
}
};

HttpServletResponseWrapper compressedResp = new HttpServletResponseWrapper(resp) {
@Override
public void setContentType(String type) {
if (mimeType != null) {
super.setContentType(mimeType);
} else {
super.setContentType(type);
}
}
};

super.doGet(compressedReq, compressedResp);
}

/**
* Check if the request URI looks like a SPA route (not a static asset)
* Static assets typically have file extensions, SPA routes don't
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package org.openmetadata.service.socket;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.net.URL;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.openmetadata.service.config.OMWebConfiguration;

public class OpenMetadataAssetServletTest {

private OpenMetadataAssetServlet servlet;

@Mock private HttpServletRequest request;
@Mock private HttpServletResponse response;
@Mock private ServletContext servletContext;
@Mock private OMWebConfiguration webConfiguration;
@Mock private ServletOutputStream outputStream;

@BeforeEach
public void setup() throws Exception {
MockitoAnnotations.openMocks(this);
when(request.getServletContext()).thenReturn(servletContext);
when(response.getOutputStream()).thenReturn(outputStream);

// Initialize servlet with /assets as resource path
servlet = new OpenMetadataAssetServlet("/", "/assets", "/", "index.html", webConfiguration);
}

@Test
public void testServeGzipAsset() throws Exception {
// Setup request for test.js
String path = "/test.js";
when(request.getRequestURI()).thenReturn(path);
when(request.getContextPath()).thenReturn("");
when(request.getPathInfo()).thenReturn(path);
when(request.getServletPath()).thenReturn("");
when(request.getHeader("Accept-Encoding")).thenReturn("gzip, deflate");
when(request.getMethod()).thenReturn("GET");
when(request.getDateHeader(anyString())).thenReturn(-1L);
when(request.getHeader("If-None-Match")).thenReturn(null);
when(request.getHeader("If-Modified-Since")).thenReturn(null);
when(servletContext.getMimeType(anyString())).thenReturn("application/javascript");

try {
servlet.doGet(request, response);
} catch (Exception e) {
e.printStackTrace();
throw e;
}

// Verify Content-Encoding header is set
verify(response).setHeader("Content-Encoding", "gzip");
// Verify Content-Type is set to javascript (not octet-stream or whatever .gz might imply)
verify(response).setContentType("application/javascript");
}

@Test
public void testServeNormalAssetIfGzipMissing() throws Exception {
// Setup request for normal.js (which has no .gz version)
String path = "/normal.js";
when(request.getRequestURI()).thenReturn(path);
when(request.getContextPath()).thenReturn("");
when(request.getPathInfo()).thenReturn(path);
when(request.getServletPath()).thenReturn("");
when(request.getHeader("Accept-Encoding")).thenReturn("gzip, deflate");
when(request.getMethod()).thenReturn("GET");
when(request.getDateHeader(anyString())).thenReturn(-1L);
when(request.getHeader("If-None-Match")).thenReturn(null);
when(request.getHeader("If-Modified-Since")).thenReturn(null);

servlet.doGet(request, response);

// Verify Content-Encoding is NOT set
verify(response, never()).setHeader(eq("Content-Encoding"), anyString());
}

@Test
public void testServeNormalAssetIfGzipNotAccepted() throws Exception {
// Setup request for test.js (which HAS .gz version)
String path = "/test.js";
when(request.getRequestURI()).thenReturn(path);
when(request.getContextPath()).thenReturn("");
when(request.getPathInfo()).thenReturn(path);
when(request.getServletPath()).thenReturn("");
when(request.getHeader("Accept-Encoding")).thenReturn(null);
when(request.getMethod()).thenReturn("GET");
when(request.getDateHeader(anyString())).thenReturn(-1L);
when(request.getHeader("If-None-Match")).thenReturn(null);
when(request.getHeader("If-Modified-Since")).thenReturn(null);

servlet.doGet(request, response);

// Verify Content-Encoding is NOT set
verify(response, never()).setHeader(eq("Content-Encoding"), anyString());
}

@Test
public void testServeBrotliAsset() throws Exception {
// Setup request for test.js (which has .br version)
String path = "/test.js";
when(request.getRequestURI()).thenReturn(path);
when(request.getContextPath()).thenReturn("");
when(request.getPathInfo()).thenReturn(path);
when(request.getServletPath()).thenReturn("");
when(request.getHeader("Accept-Encoding")).thenReturn("br"); // Only asking for br
when(request.getMethod()).thenReturn("GET");
when(request.getDateHeader(anyString())).thenReturn(-1L);
when(request.getHeader("If-None-Match")).thenReturn(null);
when(request.getHeader("If-Modified-Since")).thenReturn(null);
when(servletContext.getMimeType(anyString())).thenReturn("application/javascript");

try {
servlet.doGet(request, response);
} catch (Exception e) {
e.printStackTrace();
throw e;
}

// Verify Content-Encoding is br
verify(response).setHeader("Content-Encoding", "br");
verify(response).setContentType("application/javascript");
}

@Test
public void testPrioritizeBrotliOverGzip() throws Exception {
// Setup request for test.js (which has BOTH .br and .gz)
String path = "/test.js";
when(request.getRequestURI()).thenReturn(path);
when(request.getContextPath()).thenReturn("");
when(request.getPathInfo()).thenReturn(path);
when(request.getServletPath()).thenReturn("");
when(request.getHeader("Accept-Encoding")).thenReturn("gzip, deflate, br"); // Asking for both
when(request.getMethod()).thenReturn("GET");
when(request.getDateHeader(anyString())).thenReturn(-1L);
when(request.getHeader("If-None-Match")).thenReturn(null);
when(request.getHeader("If-Modified-Since")).thenReturn(null);
when(servletContext.getMimeType(anyString())).thenReturn("application/javascript");

try {
servlet.doGet(request, response);
} catch (Exception e) {
e.printStackTrace();
throw e;
}

// Verify Content-Encoding is br (Prioritized)
verify(response).setHeader("Content-Encoding", "br");
verify(response).setContentType("application/javascript");
}

@Test
public void testFallbackToGzipIfBrotliMissing() throws Exception {
// Setup request for gzip_only.js (which has .gz but NO .br)
String path = "/gzip_only.js";
when(request.getRequestURI()).thenReturn(path);
when(request.getContextPath()).thenReturn("");
when(request.getPathInfo()).thenReturn(path);
when(request.getServletPath()).thenReturn("");
when(request.getHeader("Accept-Encoding")).thenReturn("gzip, deflate, br"); // Asking for both
when(request.getMethod()).thenReturn("GET");
when(request.getDateHeader(anyString())).thenReturn(-1L);
when(request.getHeader("If-None-Match")).thenReturn(null);
when(request.getHeader("If-Modified-Since")).thenReturn(null);
when(servletContext.getMimeType(anyString())).thenReturn("application/javascript");

try {
servlet.doGet(request, response);
} catch (Exception e) {
e.printStackTrace();
throw e;
}

// Verify Content-Encoding is gzip (Fallback)
verify(response).setHeader("Content-Encoding", "gzip");
verify(response).setContentType("application/javascript");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('gzip only');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('gzip only compressed');
1 change: 1 addition & 0 deletions openmetadata-service/src/test/resources/assets/normal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('only normal');
1 change: 1 addition & 0 deletions openmetadata-service/src/test/resources/assets/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('normal');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('brotli compressed');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('compressed');
1 change: 1 addition & 0 deletions openmetadata-ui/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
</resources>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>gz</nonFilteredFileExtension>
<nonFilteredFileExtension>br</nonFilteredFileExtension>
<nonFilteredFileExtension>woff</nonFilteredFileExtension>
<nonFilteredFileExtension>woff2</nonFilteredFileExtension>
<nonFilteredFileExtension>ttf</nonFilteredFileExtension>
Expand Down
13 changes: 9 additions & 4 deletions openmetadata-ui/src/main/resources/ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,15 @@ export default defineConfig(({ mode }) => {
},
}),
mode === 'production' &&
viteCompression({
algorithm: 'gzip',
ext: '.gz',
}),
viteCompression({
algorithm: 'gzip',
ext: '.gz',
}),
mode === 'production' &&
viteCompression({
algorithm: 'brotliCompress',
ext: '.br',
}),
].filter(Boolean),

resolve: {
Expand Down
Loading