docs: public HttpMessage handleApiRequest (HttpRequestHeader requestHeader, HttpInputStream httpIn, 
  			HttpOutputStream httpOut, boolean force) throws IOException {
  		
  		String url = requestHeader.getURI().toString();
  		Format format = Format.OTHER;
  		ApiImplementor callbackImpl = null;
  		ApiImplementor shortcutImpl = null;
  		
  		// Check for callbacks
  		if (url.contains(CALL_BACK_URL)) {
  			logger.debug("handleApiRequest Callback: " + url);
  			for (Entry<String, ApiImplementor> callback : callBacks.entrySet()) {
  				if (url.startsWith(callback.getKey())) {
  					callbackImpl = callback.getValue();
  					break;
  				}
  			}
  			if (callbackImpl == null) {
  				logger.warn("Request to callback URL " + requestHeader.getURI().toString() + " from " +
  							requestHeader.getSenderAddress().getHostAddress() + 
  							" not found - this could be a callback url from a previous session or possibly an attempt to attack ZAP");
  				return new HttpMessage();
  			}
  		}
  		String path = requestHeader.getURI().getPath();
  		if (path != null) {
  			for (Entry<String, ApiImplementor> shortcut : shortcuts.entrySet()) {
  				if (path.startsWith(shortcut.getKey())) {
  					shortcutImpl = shortcut.getValue();
  					break;
  				}
  			}
  		}
  		
  		if (shortcutImpl == null && callbackImpl == null && ! url.startsWith(API_URL) && ! url.startsWith(API_URL_S) && ! force) {
  			return null;
  		}
  		if (callbackImpl == null && ! isPermittedAddr(requestHeader)) {
  			// Callback by their very nature are on the target domain
  			return new HttpMessage();
  		}
  		if (getOptionsParamApi().isSecureOnly() && ! requestHeader.isSecure()) {
  			// Insecure request with secure only set, always ignore
  			logger.debug("handleApiRequest rejecting insecure request");
  			return new HttpMessage();
  		}
  			
  		logger.debug("handleApiRequest " + url);
  
  		HttpMessage msg = new HttpMessage();
  		msg.setRequestHeader(requestHeader);
  		if (requestHeader.getContentLength() > 0) {
  			msg.setRequestBody(httpIn.readRequestBody(requestHeader));
  		}
  		String component = null;
  		ApiImplementor impl = null;
  		RequestType reqType = null;
  		String contentType = "text/plain; charset=UTF-8";
  		String response = "";
  		String name = null;
  		boolean error = false;
  		
  		try {
  
  			if (shortcutImpl != null) {
  				if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) {
  					if ( ! this.hasValidKey(requestHeader, getParams(requestHeader))) {
  						throw new ApiException(ApiException.Type.BAD_API_KEY);
  					}
  				}
  				msg = shortcutImpl.handleShortcut(msg);
  				impl = shortcutImpl;
  			} else if (callbackImpl != null) {
  				// Callbacks have suitably random URLs and therefore don't require keys/nonces
  				response = callbackImpl.handleCallBack(msg);
  				impl = callbackImpl;
  			} else {
  				JSONObject params = getParams(requestHeader);
  			
  				// Parse the query:
  				// format of url is http://zap/format/component/reqtype/name/?params
  				//                    0  1  2    3        4        5      6
  				String[] elements = url.split("/");
  
  				if (elements.length > 3 && elements[3].equalsIgnoreCase("favicon.ico")) {
  					// Treat the favicon as a special case:)
  					if (!getOptionsParamApi().isUiEnabled()) {
  						throw new ApiException(ApiException.Type.DISABLED);
  					}
  					InputStream is = API.class.getResourceAsStream("/resource/zap.ico");
  			    	byte[] icon = new byte[is.available()];
  			    	is.read(icon);
  			    	is.close();
  
  			    	msg.setResponseHeader(getDefaultResponseHeader(contentType));
  			    	msg.getResponseHeader().setContentLength(icon.length);
  			    	httpOut.write(msg.getResponseHeader());
  			    	httpOut.write(icon);
  					httpOut.flush();
  					httpOut.close();
  					httpIn.close();
  					return msg;
  					
  				} else if (elements.length > 3) {
  					try {
  						format = Format.valueOf(elements[3].toUpperCase());
  						switch (format) {
  						case JSONP: contentType = "application/javascript; charset=UTF-8";
  									break;
  						case XML:	contentType = "text/xml; charset=UTF-8";
  									break;
  						case HTML:	contentType = "text/html; charset=UTF-8";
  									break;
  						case UI:	contentType = "text/html; charset=UTF-8";
  									break;
  						case JSON:
  						default:
  							contentType = "application/json; charset=UTF-8";
  							break;
  						}
  					} catch (IllegalArgumentException e) {
  						format = Format.HTML;
  						throw new ApiException(ApiException.Type.BAD_FORMAT, e);
  					}
  				}
  				if (elements.length > 4) {
  					component = elements[4];
  					impl = implementors.get(component);
  					if (impl == null) {
  						throw new ApiException(ApiException.Type.NO_IMPLEMENTOR);
  					}
  				}
  				if (elements.length > 5) {
  					try {
  						reqType = RequestType.valueOf(elements[5]);
  					} catch (IllegalArgumentException e) {
  						throw new ApiException(ApiException.Type.BAD_TYPE, e);
  					}
  				}
  				if (elements.length > 6) {
  					name = elements[6];
  					if (name != null && name.indexOf("?") > 0) {
  						name = name.substring(0, name.indexOf("?"));
  					}
  				}
  				
  				if (format.equals(Format.UI)) {
  					if ( ! isEnabled() || !getOptionsParamApi().isUiEnabled()) {
  						throw new ApiException(ApiException.Type.DISABLED);
  					}
  
  					response = webUI.handleRequest(component, impl, reqType, name);
  					contentType = "text/html; charset=UTF-8";
  				} else if (name != null) {
  					if ( ! isEnabled()) {
  						throw new ApiException(ApiException.Type.DISABLED);
  					}
  					// Do this now as it might contain the api key/nonce
  					if (requestHeader.getMethod().equalsIgnoreCase(HttpRequestHeader.POST)) {
  						String contentTypeHeader = requestHeader.getHeader(HttpHeader.CONTENT_TYPE);
  						if (contentTypeHeader != null
  								&& contentTypeHeader.equals(HttpHeader.FORM_URLENCODED_CONTENT_TYPE)) {
  							params = getParams(msg.getRequestBody().toString());
  						} else {
  							throw new ApiException(ApiException.Type.CONTENT_TYPE_NOT_SUPPORTED);
  						}
  					}
  					
  					if (format.equals(Format.JSONP)) {
  						if (!getOptionsParamApi().isEnableJSONP()) {
  							// Not enabled
  							throw new ApiException(ApiException.Type.DISABLED);
  						}
  						if (! this.hasValidKey(requestHeader, params)) {
  							// An api key is required for ALL JSONP requests
  							throw new ApiException(ApiException.Type.BAD_API_KEY);
  						}
  					}
  
  					ApiResponse res;
  					switch (reqType) {
  					case action:
  						if (!getOptionsParamApi().isDisableKey()) {
  							if ( ! this.hasValidKey(requestHeader, params)) {
  								throw new ApiException(ApiException.Type.BAD_API_KEY);
  							}
  						}
  						validateFormatForViewAction(format);
  
  						ApiAction action = impl.getApiAction(name);
  						validateMandatoryParams(params, action);
  
  						res = impl.handleApiOptionAction(name, params);	
  						if (res == null) {
  							res = impl.handleApiAction(name, params);
  						}
  						response = convertViewActionApiResponse(format, name, res);
  							
  						break;
  					case view:		
  						if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) {
  							if ( ! this.hasValidKey(requestHeader, params)) {
  								throw new ApiException(ApiException.Type.BAD_API_KEY);
  							}
  						}
  						validateFormatForViewAction(format);
  
  						ApiView view = impl.getApiView(name);
  						validateMandatoryParams(params, view);
  
  						res = impl.handleApiOptionView(name, params);	
  						if (res == null) {
  							res = impl.handleApiView(name, params);
  						}
  						response = convertViewActionApiResponse(format, name, res);
  
  						break;
  					case other:
  						ApiOther other = impl.getApiOther(name);
  						if (other != null) {
  							// Checking for null to handle option actions
  							if (!getOptionsParamApi().isDisableKey() && 
  									(!getOptionsParamApi().isNoKeyForSafeOps() ||
  										other.isRequiresApiKey())) {
  								// Check if a valid api key has been used
  								if ( ! this.hasValidKey(requestHeader, params)) {
  									throw new ApiException(ApiException.Type.BAD_API_KEY);
  								}
  							}
  							validateMandatoryParams(params, other);
  						}
  						msg = impl.handleApiOther(msg, name, params);
  						break;
  					case pconn:
  						if (impl != null) {
  							ApiPersistentConnection pconn = impl.getApiPersistentConnection(name);
  							if (pconn != null) {
  								if (!getOptionsParamApi().isDisableKey() && !getOptionsParamApi().isNoKeyForSafeOps()) {
  									if ( ! this.hasValidKey(requestHeader, params)) {
  										throw new ApiException(ApiException.Type.BAD_API_KEY);
  									}
  								}
  								validateMandatoryParams(params, pconn);
  							}
  							impl.handleApiPersistentConnection(msg, httpIn, httpOut, name, params);
  						}
  						return new HttpMessage();
  					}
  				} else {
  					// Handle default front page, unless if the API UI is disabled
  					if ( ! isEnabled() || !getOptionsParamApi().isUiEnabled()) {
  						throw new ApiException(ApiException.Type.DISABLED);
  					}
  					response = webUI.handleRequest(requestHeader.getURI(), this.isEnabled());
  					format = Format.UI;
  					contentType = "text/html; charset=UTF-8";
  				}
  			}
  			
  		} catch (Exception e) {
  			if (! getOptionsParamApi().isReportPermErrors()) {
  				if (e instanceof ApiException) {
  					ApiException exception = (ApiException) e;
  					if (exception.getType().equals(ApiException.Type.DISABLED) ||
  							exception.getType().equals(ApiException.Type.BAD_API_KEY)) {
  						// Fail silently
  						return new HttpMessage();
  					}
  				}
  			}
  			handleException(msg, reqType, format, contentType, e);
  			error = true;
  		}
  		
  		if (!error && ! format.equals(Format.OTHER) && shortcutImpl == null && callbackImpl == null) {
  	    	msg.setResponseHeader(getDefaultResponseHeader(contentType));
  	    	msg.setResponseBody(response);
  	    	msg.getResponseHeader().setContentLength(msg.getResponseBody().length());
  		}
  		
  		if (impl != null) {
  			impl.addCustomHeaders(name, reqType, msg);
  		}
  
      	httpOut.write(msg.getResponseHeader());
      	httpOut.write(msg.getResponseBody().getBytes());
  		httpOut.flush();
  		if (!msg.isWebSocketUpgrade()) {
  			httpOut.close();
  			httpIn.close();
  		}
  		
  		return msg;
  	}