diff --git a/app/client/public/index.html b/app/client/public/index.html
index e8588fa4f890..d96361c0cf20 100755
--- a/app/client/public/index.html
+++ b/app/client/public/index.html
@@ -257,7 +257,7 @@
: LOG_LEVELS[1],
cloudHosting: CLOUD_HOSTING,
appVersion: {
- id: parseConfig('{{env "APPSMITH_VERSION_ID"}}'),
+ id: parseConfig('{{env "APPSMITH_VERSION_ID"}}') || "UNKNOWN",
sha: parseConfig('{{env "APPSMITH_VERSION_SHA"}}'),
releaseDate: parseConfig('{{env "APPSMITH_VERSION_RELEASE_DATE"}}'),
},
diff --git a/app/client/src/api/interceptors/request/addVersionHeader.ts b/app/client/src/api/interceptors/request/addVersionHeader.ts
new file mode 100644
index 000000000000..bc4c20b32ab7
--- /dev/null
+++ b/app/client/src/api/interceptors/request/addVersionHeader.ts
@@ -0,0 +1,13 @@
+import type { InternalAxiosRequestConfig } from "axios";
+
+export const addVersionHeader = (
+ config: InternalAxiosRequestConfig,
+ options: { version: string },
+) => {
+ const { version } = options;
+
+ config.headers = config.headers || {};
+ config.headers["X-Appsmith-Version"] = version;
+
+ return config;
+};
diff --git a/app/client/src/api/interceptors/request/apiRequestInterceptor.ts b/app/client/src/api/interceptors/request/apiRequestInterceptor.ts
index 8f7919c067dc..5371cbbcf23c 100644
--- a/app/client/src/api/interceptors/request/apiRequestInterceptor.ts
+++ b/app/client/src/api/interceptors/request/apiRequestInterceptor.ts
@@ -9,6 +9,7 @@ import getQueryParamsObject from "utils/getQueryParamsObject";
import { addRequestedByHeader } from "./addRequestedByHeader";
import { increaseGitApiTimeout } from "./increaseGitApiTimeout";
import { getCurrentGitBranch } from "selectors/gitSyncSelectors";
+import { addVersionHeader as _addVersionHeader } from "./addVersionHeader";
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
import { addGitBranchHeader as _addGitBranchHeader } from "./addGitBranchHeader";
import { addPerformanceMonitoringHeaders } from "./addPerformanceMonitoringHeaders";
@@ -49,10 +50,18 @@ const addAnonymousUserIdHeader = (config: InternalAxiosRequestConfig) => {
return _addAnonymousUserIdHeader(config, { anonymousId, segmentEnabled });
};
+const addVersionHeader = (config: InternalAxiosRequestConfig) => {
+ const appsmithConfig = getAppsmithConfigs();
+ const version = appsmithConfig.appVersion.id;
+
+ return _addVersionHeader(config, { version });
+};
+
export const apiRequestInterceptor = (config: InternalAxiosRequestConfig) => {
const interceptorPipeline = compose(
blockAirgappedRoutes,
addRequestedByHeader,
+ addVersionHeader,
addGitBranchHeader,
increaseGitApiTimeout,
addEnvironmentHeader,
diff --git a/app/client/src/api/interceptors/response/apiFailureResponseInterceptor.ts b/app/client/src/api/interceptors/response/apiFailureResponseInterceptor.ts
index eae6f6162126..edf2d1975c87 100644
--- a/app/client/src/api/interceptors/response/apiFailureResponseInterceptor.ts
+++ b/app/client/src/api/interceptors/response/apiFailureResponseInterceptor.ts
@@ -15,6 +15,7 @@ export const apiFailureResponseInterceptor = async (
failureHandlers.handleServerError,
failureHandlers.handleUnauthorizedError,
failureHandlers.handleNotFoundError,
+ failureHandlers.handleBadRequestError,
];
for (const handler of handlers) {
diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleBadRequestError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleBadRequestError.ts
new file mode 100644
index 000000000000..e7aa3f3f334e
--- /dev/null
+++ b/app/client/src/api/interceptors/response/failureHandlers/handleBadRequestError.ts
@@ -0,0 +1,30 @@
+import type { AxiosError } from "axios";
+import type { ApiResponse } from "../../../ApiResponses";
+import { handleVersionMismatch } from "sagas/WebsocketSagas/versionUpdatePrompt";
+import { getAppsmithConfigs } from "ee/configs";
+import { SERVER_ERROR_CODES } from "ee/constants/ApiConstants";
+
+export const handleBadRequestError = async (error: AxiosError) => {
+ const errorCode = error?.response?.data?.responseMeta.error?.code;
+
+ if (
+ error?.response?.status === 400 &&
+ SERVER_ERROR_CODES.VERSION_MISMATCH.includes("" + errorCode)
+ ) {
+ const responseData = error?.response?.data;
+ const message = responseData?.responseMeta.error?.message;
+ const serverVersion = (responseData?.data as { serverVersion: string })
+ .serverVersion;
+
+ handleVersionMismatch(getAppsmithConfigs().appVersion.id, serverVersion);
+
+ return Promise.reject({
+ ...error,
+ clientDefinedError: true,
+ statusCode: errorCode,
+ message,
+ });
+ }
+
+ return null;
+};
diff --git a/app/client/src/api/interceptors/response/failureHandlers/index.ts b/app/client/src/api/interceptors/response/failureHandlers/index.ts
index 2352095f1deb..bf0fe01621e8 100644
--- a/app/client/src/api/interceptors/response/failureHandlers/index.ts
+++ b/app/client/src/api/interceptors/response/failureHandlers/index.ts
@@ -4,6 +4,7 @@ export { handleCancelError } from "./handleCancelError";
export { handleOfflineError } from "./handleOfflineError";
export { handleTimeoutError } from "./handleTimeoutError";
export { handleNotFoundError } from "./handleNotFoundError";
+export { handleBadRequestError } from "./handleBadRequestError";
export { handleUnauthorizedError } from "./handleUnauthorizedError";
export { handleExecuteActionError } from "./handleExecuteActionError";
export { handleMissingResponseMeta } from "./handleMissingResponseMeta";
diff --git a/app/client/src/ce/constants/ApiConstants.tsx b/app/client/src/ce/constants/ApiConstants.tsx
index b1c7a6c7ee26..be043d802b76 100644
--- a/app/client/src/ce/constants/ApiConstants.tsx
+++ b/app/client/src/ce/constants/ApiConstants.tsx
@@ -64,6 +64,7 @@ export const SERVER_ERROR_CODES = {
"AE-APP-4013",
],
UNABLE_TO_FIND_PAGE: ["AE-APP-4027", "AE-USR-4004"],
+ VERSION_MISMATCH: ["AE-BAD-4002"],
};
export enum ERROR_CODES {
diff --git a/app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.tsx b/app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.tsx
index 600b4fc5671d..c5cf5d863d0d 100644
--- a/app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.tsx
+++ b/app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.tsx
@@ -2,8 +2,6 @@ import { put } from "redux-saga/effects";
import { APP_LEVEL_SOCKET_EVENTS } from "./socketEvents";
import { collabSetAppEditors } from "actions/appCollabActions";
-import { getAppsmithConfigs } from "ee/configs";
-import { handleVersionUpdate } from "./versionUpdatePrompt";
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -13,15 +11,6 @@ export default function* handleAppLevelSocketEvents(event: any) {
case APP_LEVEL_SOCKET_EVENTS.LIST_ONLINE_APP_EDITORS: {
yield put(collabSetAppEditors(event.payload[0]));
- return;
- }
- // notification on release version
- case APP_LEVEL_SOCKET_EVENTS.RELEASE_VERSION_NOTIFICATION: {
- const { appVersion } = getAppsmithConfigs();
- const [serverVersion] = event.payload;
-
- handleVersionUpdate(appVersion, serverVersion);
-
return;
}
}
diff --git a/app/client/src/sagas/WebsocketSagas/versionUpdatePrompt.ts b/app/client/src/sagas/WebsocketSagas/versionUpdatePrompt.ts
index 4b28ab8c8ae4..a8edc806de20 100644
--- a/app/client/src/sagas/WebsocketSagas/versionUpdatePrompt.ts
+++ b/app/client/src/sagas/WebsocketSagas/versionUpdatePrompt.ts
@@ -1,111 +1,37 @@
-// Check if user is updating the app when toast is shown
-// Check how many times does the user see a toast before updating
-
import { toast } from "@appsmith/ads";
import {
createMessage,
INFO_VERSION_MISMATCH_FOUND_RELOAD_REQUEST,
} from "ee/constants/messages";
-import type { AppVersionData } from "ee/configs/types";
-import {
- getVersionUpdateState,
- removeVersionUpdateState,
- setVersionUpdateState,
-} from "utils/storage";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
-enum UpdateStateEvent {
- PROMPT_SHOWN = "PROMPT_SHOWN",
- UPDATE_REQUESTED = "UPDATE_REQUESTED",
+function handleUpdateRequested(fromVersion: string, toVersion: string) {
+ AnalyticsUtil.logEvent("VERSION_UPDATE_REQUESTED", {
+ fromVersion,
+ toVersion,
+ });
+ // Reload to fetch the latest app version
+ location.reload();
}
-export interface VersionUpdateState {
- currentVersion: string;
- upgradeVersion: string;
- timesShown: number;
- event: UpdateStateEvent;
-}
+export async function handleVersionMismatch(
+ currentVersion: string,
+ serverVersion: string,
+) {
+ // If no version is set, ignore
+ if (!currentVersion) return;
-let timesShown = 0;
+ AnalyticsUtil.logEvent("VERSION_UPDATE_SHOWN", {
+ fromVersion: currentVersion,
+ toVersion: serverVersion,
+ });
-function showPrompt(newUpdateState: VersionUpdateState) {
toast.show(createMessage(INFO_VERSION_MISMATCH_FOUND_RELOAD_REQUEST), {
kind: "info",
autoClose: false,
action: {
text: "refresh",
- effect: () => handleUpdateRequested(newUpdateState),
+ effect: () => handleUpdateRequested(currentVersion, serverVersion),
},
});
}
-
-function handleUpdateRequested(newUpdateState: VersionUpdateState) {
- // store version update with timesShown counter
- setVersionUpdateState({
- ...newUpdateState,
- event: UpdateStateEvent.UPDATE_REQUESTED,
- }).then(() => {
- AnalyticsUtil.logEvent("VERSION_UPDATE_REQUESTED", {
- fromVersion: newUpdateState.currentVersion,
- toVersion: newUpdateState.upgradeVersion,
- timesShown,
- });
- // Reload to fetch the latest app version
- location.reload();
- });
-}
-
-export async function handleVersionUpdate(
- currentVersionData: AppVersionData,
- serverVersion: string,
-) {
- const { edition, id: currentVersion } = currentVersionData;
-
- // If no version is set, ignore
- if (!currentVersion) return;
-
- const versionState: VersionUpdateState | null = await getVersionUpdateState();
-
- if (currentVersion === serverVersion) {
- if (versionState) {
- AnalyticsUtil.logEvent("VERSION_UPDATE_SUCCESS", {
- fromVersion: versionState.currentVersion,
- toVersion: versionState.upgradeVersion,
- edition,
- });
- await removeVersionUpdateState();
- }
- }
-
- if (currentVersion !== serverVersion) {
- if (versionState) {
- timesShown = versionState.timesShown;
-
- if (
- currentVersion === versionState.currentVersion &&
- versionState.event === UpdateStateEvent.UPDATE_REQUESTED
- ) {
- AnalyticsUtil.logEvent("VERSION_UPDATED_FAILED", {
- fromVersion: versionState.currentVersion,
- toVersion: versionState.upgradeVersion,
- edition,
- });
- }
- }
-
- const newUpdateState: VersionUpdateState = {
- currentVersion,
- upgradeVersion: serverVersion,
- // Increment the timesShown counter
- timesShown: timesShown + 1,
- event: UpdateStateEvent.PROMPT_SHOWN,
- };
-
- AnalyticsUtil.logEvent("VERSION_UPDATE_SHOWN", {
- fromVersion: currentVersion,
- toVersion: serverVersion,
- timesShown,
- });
- showPrompt(newUpdateState);
- }
-}
diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts
index dff7c051aaf3..8893261bb190 100644
--- a/app/client/src/utils/storage.ts
+++ b/app/client/src/utils/storage.ts
@@ -1,7 +1,6 @@
import log from "loglevel";
import moment from "moment";
import localforage from "localforage";
-import type { VersionUpdateState } from "../sagas/WebsocketSagas/versionUpdatePrompt";
import { isNumber } from "lodash";
import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig";
import type { EditorViewMode } from "ee/entities/IDE/constants";
@@ -761,25 +760,6 @@ export const isUserSignedUpFlagSet = async (email: string) => {
}
};
-export const setVersionUpdateState = async (state: VersionUpdateState) => {
- try {
- await store.setItem(STORAGE_KEYS.VERSION_UPDATE_STATE, state);
- } catch (e) {
- log.error("An error occurred while storing version update state", e);
- }
-};
-
-export const getVersionUpdateState =
- async (): Promise => {
- return await store.getItem(
- STORAGE_KEYS.VERSION_UPDATE_STATE,
- );
- };
-
-export const removeVersionUpdateState = async () => {
- return store.removeItem(STORAGE_KEYS.VERSION_UPDATE_STATE);
-};
-
export const getAppKbState = async (appId: string) => {
try {
const aiKBApplicationMap: Record<
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java
index 2c7bd27e2cb4..f44711f4c82a 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java
@@ -1,5 +1,6 @@
package com.appsmith.server.configurations;
+import com.appsmith.external.exceptions.ErrorDTO;
import com.appsmith.server.authentication.handlers.AccessDeniedHandler;
import com.appsmith.server.authentication.handlers.CustomServerOAuth2AuthorizationRequestResolver;
import com.appsmith.server.authentication.handlers.LogoutSuccessHandler;
@@ -8,6 +9,7 @@
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ResponseDTO;
+import com.appsmith.server.exceptions.AppsmithErrorCode;
import com.appsmith.server.filters.CSRFFilter;
import com.appsmith.server.filters.ConditionalFilter;
import com.appsmith.server.filters.LoginRateLimitFilter;
@@ -24,6 +26,7 @@
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.InvalidMediaTypeException;
@@ -114,6 +117,9 @@ public class SecurityConfig {
@Autowired
private CustomOauth2ClientRepositoryManager oauth2ClientManager;
+ @Autowired
+ private ProjectProperties projectProperties;
+
@Value("${appsmith.internal.password}")
private String INTERNAL_PASSWORD;
@@ -284,10 +290,12 @@ private User createAnonymousUser() {
}
private Mono sanityCheckFilter(ServerWebExchange exchange, WebFilterChain chain) {
+ final HttpHeaders headers = exchange.getRequest().getHeaders();
+
// 1. Check if the content-type is valid at all. Mostly just checks if it contains a `/`.
MediaType contentType;
try {
- contentType = exchange.getRequest().getHeaders().getContentType();
+ contentType = headers.getContentType();
} catch (InvalidMediaTypeException e) {
return writeErrorResponse(exchange, chain, e.getMessage());
}
@@ -300,6 +308,15 @@ private Mono sanityCheckFilter(ServerWebExchange exchange, WebFilterChain
return writeErrorResponse(exchange, chain, "Unsupported Content-Type");
}
+ // 3. Check Appsmith version, if present. Not making this a mandatory check for now, but reconsider later.
+ final String versionHeaderValue = headers.getFirst("X-Appsmith-Version");
+ final String serverVersion = projectProperties.getVersion();
+ if (versionHeaderValue != null && !serverVersion.equals(versionHeaderValue)) {
+ final ErrorDTO error = new ErrorDTO(
+ AppsmithErrorCode.VERSION_MISMATCH.getCode(), AppsmithErrorCode.VERSION_MISMATCH.getDescription());
+ return writeErrorResponse(exchange, chain, error, new VersionMismatchData(serverVersion));
+ }
+
return chain.filter(exchange);
}
@@ -315,4 +332,24 @@ private Mono writeErrorResponse(ServerWebExchange exchange, WebFilterChain
return chain.filter(exchange);
}
}
+
+ private Mono writeErrorResponse(
+ ServerWebExchange exchange, WebFilterChain chain, ErrorDTO error, T data) {
+ final ServerHttpResponse response = exchange.getResponse();
+ final HttpStatus status = HttpStatus.BAD_REQUEST;
+ response.setStatusCode(status);
+ response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+
+ final ResponseDTO responseBody = new ResponseDTO<>(status.value(), error);
+ responseBody.setData(data);
+
+ try {
+ return response.writeWith(
+ Mono.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(responseBody))));
+ } catch (JsonProcessingException ex) {
+ return chain.filter(exchange);
+ }
+ }
+
+ record VersionMismatchData(String serverVersion) {}
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java
index 17682a720047..4824f9815e22 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java
@@ -67,6 +67,7 @@ public enum AppsmithErrorCode {
NAME_CLASH_NOT_ALLOWED_IN_REFACTOR("AE-AST-4009", "Name clash not allowed in refactor"),
GENERIC_BAD_REQUEST("AE-BAD-4000", "Generic bad request"),
MALFORMED_REQUEST("AE-BAD-4001", "Malformed request body"),
+ VERSION_MISMATCH("AE-BAD-4002", "Appsmith version mismatch"),
GOOGLE_RECAPTCHA_FAILED("AE-CAP-4035", "Google recaptcha failed"),
GOOGLE_RECAPTCHA_INVITE_FLOW_FAILED("AE-CAP-4100", "Google recaptcha failed"),
INVALID_CRUD_PAGE_REQUEST("AE-CRD-4039", "Invalid crud page request"),